When Does Spring Actually Commit? Unraveling Transaction Timing and Locking Pitfalls

This article dissects the exact moment a Spring @Transactional method commits relative to a surrounding lock, explains why committing after unlock can cause overselling, walks through the relevant Spring source code, and offers practical debugging and fixing strategies for reliable concurrent database operations.

Java Backend Technology
Java Backend Technology
Java Backend Technology
When Does Spring Actually Commit? Unraveling Transaction Timing and Locking Pitfalls

Recently a question on SegmentFault sparked a deep dive into Spring transaction timing when a method is wrapped with a manual lock to prevent overselling.

Problem Overview

A @Transactional method performs two actions: (1) query product inventory, (2) if stock exists, decrement it and insert an order record. Both actions touch the database and must be atomic.

The developer wrapped the whole method with a lock to ensure only one thread executes the stock‑decrease and order‑creation logic at a time, assuming this guarantees correctness.

Why Timing Matters

MySQL runs at the REPEATABLE‑READ isolation level. If the transaction commits after the lock is released, another thread can acquire the lock, read the stale inventory (still showing stock), and also create an order, leading to overselling.

Conversely, if the transaction commits before unlocking, the lock remains held until the commit finishes, preventing the race condition.

Investigating Spring’s Transaction Lifecycle

Switching JDBC Connection [HikariProxyConnection@...] to manual commit

This log line comes from

org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin

. Spring changes the connection’s auto‑commit flag to false, but the transaction does not start until the first SQL statement is executed (after the lock).

Thus, the transaction’s real start point is after lock.lock(), and the commit occurs in the finally block of TransactionAspectSupport#invokeWithinTransaction.

Code Path Highlights

Lock acquisition → lock.lock() Transaction start → DataSourceTransactionManager#doBegin Business logic execution

Transaction commit → AbstractPlatformTransactionManager#commit Lock release → lock.unlock() If the unlock() call happens before the commit (e.g., due to an early return or exception handling), other threads can slip in and cause duplicate orders.

Debugging Tips

Set breakpoints at the following methods to observe the exact order:

DataSourceTransactionManager#doBegin
TransactionAspectSupport#invokeWithinTransaction
AbstractPlatformTransactionManager#commit
com.mysql.cj.protocol.a.NativeProtocol#sendQueryString

(to catch the actual COMMIT SQL)

Use the call stack to locate where Spring begins the transaction and where it decides to commit or roll back.

Common Pitfalls

Using @Transactional without understanding that the transaction starts after the lock.

Releasing the lock before the transaction finishes, which leads to overselling.

Assuming con.setAutoCommit(false) immediately starts a transaction.

Solutions

Ensure the entire transaction is enclosed within the lock:

lock.lock();
try {
    // transaction starts automatically when first SQL runs
    // business logic (query, decrement, insert order)
} finally {
    // commit happens here before lock is released
    lock.unlock();
}

Alternatively, increase the isolation level to SERIALIZABLE for the method, which forces MySQL to acquire row‑level locks automatically, eliminating the need for an explicit Java lock.

For more fine‑grained control, use programmatic transactions (e.g., TransactionTemplate) so you can explicitly commit before unlocking.

Rollback‑Only Scenario

When an inner transaction throws a RuntimeException, Spring marks the outer transaction as rollback‑only. The commit path checks defStatus.isGlobalRollbackOnly() and skips committing, rolling back instead.

Transaction rolled back because it has been marked as rollback‑only

Understanding this flow helps avoid unexpected rollbacks when mixing multiple transactional methods.

Conclusion

The key takeaway is that in Spring the transaction is started after the lock acquisition and committed just before the lock is released. Aligning these two steps prevents overselling and ensures data consistency in high‑concurrency scenarios.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

concurrencylocking
Java Backend Technology
Written by

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!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.