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 {
// ...
}
Important: A rollback will only trigger if the exception "escapes" the
@Transactionalmethod. 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.
-
LockModeTypemodes:OPTIMISTIC(version check on commit) andOPTIMISTIC_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
- Misunderstanding rollback rules: Expecting a rollback for a checked exception, or conversely, committing after a
RuntimeExceptionthat was caught and swallowed inside the method. - "Poisoned" session: After an exception (e.g.,
DataIntegrityViolationException), the Hibernate session may be marked asrollback-only. Subsequent operations in this transaction will fail. The solution is to usePropagation.REQUIRES_NEWto isolate the failing operation. - Spurious
OptimisticLockException: Can occur when usingorphanRemovaland complex object graphs, where Hibernate attempts to update an already deleted entity. Sometimesem.flush()followed byem.clear()helps to synchronize the state. - Mixing strategies: The presence of
@Version(optimistic locking) does not bypass the version check even when usingSELECT ... FOR UPDATE(pessimistic locking). If another transaction modified the data, anOptimisticLockExceptionwill still be thrown upon update. - 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
- Spring rolls back only
RuntimeExceptionandErrorby default. For checked exceptions, rollback must be explicitly specified viarollbackFor. - The exception must "escape" the
@Transactionalmethod for Spring to perform a rollback. Do not swallow exceptions inside the method. - 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. - 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. -
Isolation.DEFAULTcan lead to different behavior across environments. For critical operations, specify the isolation level explicitly. -
Propagation.REQUIRES_NEWis your tool for isolating failing operations (e.g., logging) that should not affect the main transaction's rollback.
Top comments (0)