跪拜 Guibai
← All articles
Backend

Redisson Lock + @Transactional Still Corrupted Data Under Load

By 神奇小汤圆 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

This failure mode is easy to miss in code review because the logic looks correct in isolation. Any Java service that mixes declarative transactions with distributed locks can silently corrupt data under load if the lock is released before the transaction commits.

Summary

A seemingly correct combination of Redisson distributed lock and Spring's @Transactional caused inventory to go negative under high concurrency. The lock was released in a finally block inside the transactional method, but Spring's AOP proxy had not yet called connection.commit(). Another thread could acquire the lock, read the uncommitted old state, and perform its own deduction, resulting in a dirty write.

The root cause is a scope mismatch: the lock's lifecycle ended before the transaction's lifecycle. Two fixes restore correctness. The recommended approach moves the lock acquisition and release to an outer service layer, so the transactional method returns and commits before the lock is released. An alternative uses TransactionTemplate to manually commit the transaction inside the locked block before releasing the lock.

A secondary Redisson pitfall is also noted: passing an explicit leaseTime to tryLock disables the WatchDog automatic renewal mechanism, risking premature lock expiry during long-running operations.

Takeaways
Releasing a Redisson lock inside a @Transactional method creates a window where the lock is gone but the transaction has not committed, allowing concurrent threads to read stale data.
The Spring AOP proxy commits the transaction after the target method returns, so a finally-block unlock executes before the commit.
Moving lock acquisition to an outer service layer ensures the transactional method fully commits before the lock is released.
Using TransactionTemplate to commit the transaction inside the locked block before releasing the lock is an alternative fix.
Passing an explicit leaseTime to Redisson's tryLock disables the WatchDog auto-renewal, risking lock expiry during long operations.
Conclusions

The bug is a classic AOP ordering problem: developers reason about source-code order, but the proxy interleaves transaction boundaries around the method, not inside it.

Many teams treat distributed locks and database transactions as interchangeable safety nets, but their lifecycles are independent and must be deliberately sequenced.

The WatchDog detail is a sharp edge in Redisson's API design — an optional parameter silently disables a safety feature, and the default behavior is safer than the explicit one.

Concepts & terms
Spring AOP proxy
Spring wraps @Transactional beans in a proxy that starts a database transaction before the method call and commits or rolls back after the method returns. Code inside the method executes with the transaction open but not yet committed.
Redisson WatchDog
A background thread that automatically renews a Redisson distributed lock's expiry time as long as the lock-holding client is alive. It activates only when no explicit leaseTime is set on lock acquisition.
Dirty write
A concurrency anomaly where two transactions modify the same data based on stale reads, causing one transaction's write to overwrite the other's without seeing the intermediate state.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗