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

  1. Lock not released — always use try-finally; set TTL as safety net
  2. Lock expired before work done — TTL must exceed max operation time
  3. No fencing token — stale lock holder may write after release
  4. 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