In modern distributed systems, ensuring data consistency under high concurrency is one of the biggest challenges. While databases provide transaction guarantees, when multiple threads or services interact concurrently, we often rely on distributed locks to coordinate access to shared resources.
One popular choice is Redlock, a distributed locking algorithm implemented on top of Redis. It provides fault tolerance and fairness in distributed environments. However, using Redlock correctly requires more than just acquiring and releasing locks—it demands careful alignment with database transactions.
Recently, I encountered a real-world issue in a Spring Boot application where Redlock inadvertently caused a lost update anomaly. Here’s the story of how I debugged it, understood the root cause, and implemented a solution with custom AOP that made the system resilient.
The Problem: Lost Update with Redlock
The system was designed to handle concurrent requests, with Redlock ensuring only one thread could modify a shared entity at a time. On paper, this should have prevented conflicts.
But under load, I noticed something peculiar:
Thread A acquired the lock and updated the database.
Before Thread A’s transaction committed, the lock was released.
Thread B acquired the lock and read stale data, then applied its update.
When Thread A’s transaction finally committed, Thread B’s update was overwritten — a textbook lost update anomaly.
The crux of the issue was that Redlock was releasing earlier than the database commit. Distributed lock lifecycle and database transaction lifecycle were not synchronized.
Why This Happens
Database transactions are managed by Spring (via @Transactional). A transaction commits after the annotated method finishes.
Redlock, however, is external. Once the method body completes, many implementations release the lock—without waiting for the DB commit.
This small timing gap creates a window where other threads can jump in, causing inconsistent writes.
This is a classic example of how distributed locks and transactional boundaries must be coordinated to achieve correctness.
The Solution: Custom AOP with Transaction Synchronization
To fix the problem, I created a custom Aspect-Oriented Programming (AOP) layer that hooks into the transaction lifecycle.
The idea was simple:
Acquire the Redis lock before executing the critical section.
Bind the lock’s release to the DB commit phase.
Release the lock only after the transaction is fully committed.
In Spring, this can be achieved using TransactionSynchronizationManager, which provides callbacks for transaction events.
Pseudocode outline:
@Around("@annotation(RedisLock)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
String lockKey = getLockKey(pjp);
RLock lock = redissonClient.getLock(lockKey);
lock.lock(); // Acquire Redis lock
try {
// Register callback to release lock *after* commit
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCompletion () {
lock.unlock();
}
}
);
return pjp.proceed(); // Proceed with method execution
} catch (Exception e) {
lock.unlock(); // Fail-safe
throw e;
}
Benefits of This Approach
✅ No lost updates – Lock is tied directly to DB commit lifecycle.
✅ Improved reliability – Threads always operate on committed, consistent data.
✅ Seamless integration – AOP ensures minimal intrusion into business logic.
✅ Extensible – Can be adapted for other lock strategies or transaction managers.
Key Learnings
This experience reinforced some important lessons:
A distributed lock is not enough on its own — it must align with the database’s notion of “when work is done.”
In high-concurrency systems, subtle race conditions often hide in the gaps between external tools (Redis) and internal guarantees (Spring transactions).
AOP + transaction synchronization is a powerful combination to enforce consistency without cluttering business logic.
Conclusion
Distributed systems demand both correctness and performance. Tools like Redlock provide strong primitives, but they must be carefully integrated into application workflows. By synchronizing Redis locks with transaction commits, we can eliminate anomalies like lost updates and build systems that are both reliable and scalable.
If you’re building with Spring Boot + Redis, I encourage you to look deeper into how your locks interact with transaction boundaries. It could save you from subtle bugs in production.
Have you faced similar challenges with distributed locks or transaction synchronization? I’d love to hear your solutions and approaches in the comments.
Top comments (0)