Does @Transactional Commit Before Unlock? Deep Dive into Spring Transaction Timing
This article explores the precise moment a Spring @Transactional transaction is committed relative to a surrounding lock release, revealing that commits occur after the method finishes, and demonstrates how to verify this through source code inspection, debugging techniques, and proper lock‑usage to prevent overselling.
Problem Overview
A colleague posted a code snippet that first queries product inventory, then, if stock exists, decrements it and inserts an order. Both operations modify the database and should be atomic, so the method is annotated with @Transactional and wrapped in a lock to ensure only one thread modifies the stock at a time.
Why Transaction Timing Matters
MySQL uses the REPEATABLE READ isolation level by default. In a high‑concurrency scenario, if the transaction is committed after the lock is released, another thread may read stale stock data and also place an order, leading to overselling.
Two cases are considered:
If the transaction commits before unlock, the lock still protects the critical section and overselling cannot happen.
If the transaction commits after unlock, the lock no longer protects the data, and overselling is possible.
Source Code Analysis
The lock is applied around the business logic, and the @Transactional annotation triggers Spring’s transaction management. The actual transaction start occurs in
org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin, which switches the JDBC connection to manual commit (sets autoCommit to false).
Spring’s transaction interceptor (
org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction) creates the transaction before the business method runs and commits it after the method returns, unless an exception marks the transaction as rollback‑only.
Key code snippets:
@Transactional con.setAutoCommit(false) org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransactionWhen an exception occurs, Spring decides whether to roll back based on RuleBasedTransactionAttribute#rollbackOn. By default, RuntimeException and Error trigger a rollback.
Debugging Tips
To see where the transaction actually begins, set a breakpoint in DataSourceTransactionManager#doBegin and observe the log line:
Switching JDBC Connection [...] to manual commit
To verify the commit point, place a breakpoint in AbstractPlatformTransactionManager#commit. The commit will only happen if the transaction is not marked as rollback‑only.
Additional debugging tricks include:
Breakpoint on java.sql.Connection#setAutoCommit to see which implementation is used.
Breakpoint on TransactionAspectSupport#invokeWithinTransaction to trace the call stack.
Breakpoint on com.mysql.cj.protocol.a.NativeProtocol#sendQueryString to stop only on actual SQL commit statements.
Solution
The correct approach is to ensure the entire transaction is executed within the lock’s scope, i.e., acquire the lock, start the transaction, perform the business logic, commit, and finally release the lock. This guarantees that the commit happens before the lock is released.
Alternative solutions include:
Using a higher isolation level such as SERIALIZABLE to prevent overselling without explicit locks (though it may impact performance).
Programmatic transaction management to control begin, commit, and rollback manually.
Adjusting @Transactional attributes (e.g., isolation, propagation) to suit the concurrency requirements.
For distributed systems, a distributed lock (e.g., Redis or Zookeeper) should be used instead of a simple in‑process lock.
Rollback‑Only Scenario
When an inner transaction throws an exception, Spring marks the outer transaction as rollback‑only. The check defStatus.isGlobalRollbackOnly() causes the outer transaction to roll back even if the outer method completes normally.
Understanding transaction propagation (default REQUIRED) helps avoid unexpected rollbacks.
Conclusion
In the examined code, the transaction commit occurs after the method finishes, which means the lock is released before the commit. Properly nesting the lock and transaction, or using a stricter isolation level, resolves the overselling problem.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
