DEV Community

Silver_dev
Silver_dev

Posted on

Hibernate Under the Hood. Part 6: Transaction Management and Locking

Hibernate and Spring ensure data integrity during concurrent operations. It is crucial to understand two key mechanisms here: transactions, which define the boundaries of a unit of work, and locks, which resolve conflicts during concurrent access.

Transaction Management in Spring and Hibernate

At the core is the @Transactional annotation, which wraps a method in a transaction via Spring AOP (proxies) and decides when to commit or roll it back.

Key point — Rollback Rules:
By default, Spring rolls back a transaction only for unchecked exceptions (RuntimeException and Error). For checked exceptions (subclasses of Exception, but not RuntimeException), the transaction commits by default. This is a frequent source of errors: a developer throws an IOException and expects a rollback, but the data is partially saved.

// Explicit rollback management
@Transactional(
    // rollback for checked exceptions
    rollbackFor = { IOException.class, PaymentGatewayException.class },
    // commit for RuntimeException (e.g., to retry later) 
    noRollbackFor = { OptimisticLockException.class } 
)
public void processOrder(Order order) throws IOException {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Important: A rollback will only trigger if the exception "escapes" the @Transactional method. If you catch it inside the method, Spring will not know about it, and the transaction will commit.

Isolation Levels:
Spring allows you to explicitly specify the isolation level (Isolation.READ_COMMITTED, REPEATABLE_READ, etc.). By default, it uses Isolation.DEFAULT, which relies on the underlying database's default settings. This can be dangerous if different environments (e.g., staging and production) use different databases with different default levels — the code's behavior will become unpredictable.

Propagation:
Defines how a method's transaction relates to an existing one. For example, Propagation.REQUIRES_NEW always creates a new transaction, suspending the outer one. This is useful for operations that must be committed independently (e.g., audit logging).

Locking: Conflict Resolution Strategies

Hibernate, like JPA, offers two fundamentally different approaches to locking: optimistic and pessimistic.

Optimistic Locking

Based on the assumption that conflicts are rare. Transactions do not lock data during reads, but before committing, they check if anyone else has modified the data since it was read.

It is implemented via a field annotated with @Version (a number or Timestamp). On every update, Hibernate executes SQL like UPDATE ... SET version = version + 1 WHERE id = ? AND version = ?. If no rows satisfy the WHERE condition (because the version changed), an OptimisticLockException is thrown.

  • When to use: For most web applications and scenarios with rare conflicts. It is a scalable and performant approach.
  • LockModeType modes: OPTIMISTIC (version check on commit) and OPTIMISTIC_FORCE_INCREMENT (forced version increment even without actual changes).

Pessimistic Locking

Based on the assumption that conflicts are likely. Data is locked at the database level for the duration of the transaction, preventing other transactions from modifying or reading it.

It is implemented via LockModeType (e.g., PESSIMISTIC_WRITE), which translates to SQL SELECT ... FOR UPDATE.

  • When to use: For critical operations with high contention (e.g., deducting money from an account), where rollbacks and retries are undesirable or too costly.
  • Risks: Locks are held until the end of the transaction, which can lead to deadlocks and reduced throughput.

Typical Mistakes and Pitfalls

  1. Misunderstanding rollback rules: Expecting a rollback for a checked exception, or conversely, committing after a RuntimeException that was caught and swallowed inside the method.
  2. "Poisoned" session: After an exception (e.g., DataIntegrityViolationException), the Hibernate session may be marked as rollback-only. Subsequent operations in this transaction will fail. The solution is to use Propagation.REQUIRES_NEW to isolate the failing operation.
  3. Spurious OptimisticLockException: Can occur when using orphanRemoval and complex object graphs, where Hibernate attempts to update an already deleted entity. Sometimes em.flush() followed by em.clear() helps to synchronize the state.
  4. Mixing strategies: The presence of @Version (optimistic locking) does not bypass the version check even when using SELECT ... FOR UPDATE (pessimistic locking). If another transaction modified the data, an OptimisticLockException will still be thrown upon update.
  5. Long transactions: Holding a pessimistic lock or an open session during user interaction (e.g., via OpenSessionInView) is a direct path to connection pool exhaustion and deadlocks.

What to Use and When

Feature Optimistic Locking Pessimistic Locking
Principle Conflicts are rare. Check before commit. Conflicts are likely. Lock in the DB.
Implementation @Version LockModeType.PESSIMISTIC_WRITE (SELECT ... FOR UPDATE)
When to use Most web apps, CRUD, rare updates. Critical operations with high contention (finance, reservations).
Risks OptimisticLockException on conflict (requires handling/retry). Deadlocks, reduced throughput, long-held locks.

Summary

  1. Spring rolls back only RuntimeException and Error by default. For checked exceptions, rollback must be explicitly specified via rollbackFor.
  2. The exception must "escape" the @Transactional method for Spring to perform a rollback. Do not swallow exceptions inside the method.
  3. Optimistic locking (@Version) is a version check upon update. It does not lock data, but merely reports a conflict (OptimisticLockException). This is the default choice for most systems.
  4. Pessimistic locking (SELECT ... FOR UPDATE) is an actual lock at the database level. Use it sparingly, only where its absence would cause a critical data integrity error.
  5. Isolation.DEFAULT can lead to different behavior across environments. For critical operations, specify the isolation level explicitly.
  6. Propagation.REQUIRES_NEW is your tool for isolating failing operations (e.g., logging) that should not affect the main transaction's rollback.

Top comments (0)