On this page
Distributed Lock
Distributed locks coordinate access to shared resources across multiple JVM instances. They prevent race conditions when local synchronized or ReentrantLock is insufficient.
When You Need Distributed Locks
Instance 1 ──┐
Instance 2 ──┼──▶ Shared Resource (inventory, payment, cron job)
Instance 3 ──┘
Without lock: two instances may process the same order simultaneously
With lock: only one instance processes at a time
Redis-Based Lock (Redisson)
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.26.0</version>
</dependency>
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
return Redisson.create(config);
}
}
@Service
public class InventoryService {
private final RedissonClient redisson;
public void reserveStock(String productId, int quantity) {
RLock lock = redisson.getLock("lock:inventory:" + productId);
try {
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
int stock = getStock(productId);
if (stock < quantity) throw new InsufficientStockException();
updateStock(productId, stock - quantity);
} else {
throw new LockAcquisitionException("Could not acquire lock");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) lock.unlock();
}
}
}
Manual Redis Lock (SET NX)
public boolean acquireLock(String key, String value, Duration ttl) {
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(key, value, ttl);
return Boolean.TRUE.equals(acquired);
}
public void releaseLock(String key, String value) {
// Lua script for atomic check-and-delete
String script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
""";
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
List.of(key), value);
}
Always use Lua script for release — prevents deleting another instance’s lock.
ZooKeeper Lock (Curator)
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.5.0</version>
</dependency>
CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:2181",
new ExponentialBackoffRetry(1000, 3));
client.start();
InterProcessMutex lock = new InterProcessMutex(client, "/locks/order-processing");
try {
if (lock.acquire(5, TimeUnit.SECONDS)) {
processOrder(orderId);
}
} finally {
lock.release();
}
Comparison
| Feature | Redis (Redisson) | ZooKeeper (Curator) |
|---|---|---|
| Performance | Very fast | Moderate |
| Consistency | Eventually (single Redis) / Strong (RedLock) | Strong |
| Fencing tokens | Manual | Built-in (sequential nodes) |
| Complexity | Low | Higher |
| Best for | Short-lived locks, high throughput | Strict ordering, leader election |
Common Pitfalls
- Lock not released — always use try-finally; set TTL as safety net
- Lock expired before work done — TTL must exceed max operation time
- No fencing token — stale lock holder may write after release
- Reentrant issues — ensure same thread releases the lock it acquired
Best Practices
- Prefer Redisson over manual Redis lock implementation
- Always set lock TTL to prevent deadlocks from crashed instances
- Use tryLock with timeout — never block indefinitely
- Keep lock scope minimal — lock only the critical section
- Consider if you really need a lock — optimistic locking (version column) may suffice