Skip to content

Distributed Locks

Garvit Joshi edited this page Jan 17, 2026 · 3 revisions

Distributed Locks

This guide covers the fundamentals of using @DistributedLock for exclusive access control across multiple servers.

What is a Distributed Lock?

A distributed lock ensures only one instance can execute a method at a time, even across multiple servers:

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   Instance A    │     │   Instance B    │     │   Instance C    │
│                 │     │                 │     │                 │
│ processPayment()│     │ processPayment()│     │ processPayment()│
│       │         │     │       │         │     │       │         │
│       ▼         │     │       ▼         │     │       ▼         │
│  Try Lock ──────┼─────┼───────┼─────────┼─────┼───────┼         │
└────────┼────────┘     └───────┼─────────┘     └───────┼─────────┘
         │                      │                       │
         ▼                      ▼                       ▼
    ┌─────────────────────────────────────────────────────────┐
    │                         Redis                           │
    │                                                         │
    │   Lock: payment-order123                                │
    │   Holder: Instance A                                    │
    │   TTL: 10 minutes                                       │
    │                                                         │
    └─────────────────────────────────────────────────────────┘
         │                      │                       │
         ▼                      ▼                       ▼
    ┌─────────┐           ┌─────────┐             ┌─────────┐
    │ACQUIRED │           │ DENIED  │             │ DENIED  │
    │Execute  │           │ Skip or │             │ Skip or │
    │ Method  │           │  Wait   │             │  Wait   │
    └─────────┘           └─────────┘             └─────────┘

Your First Lock

Add @DistributedLock to any Spring-managed bean method:

@Service
public class MyService {

    @DistributedLock(key = "my-task")
    public void doSomething() {
        // This code runs on only one instance at a time
        System.out.println("Executing critical task...");
    }
}

When doSomething() is called:

  1. Locksmith tries to acquire lock lock:my-task in Redis
  2. If successful, the method executes
  3. After completion (or exception), the lock is released
  4. If the lock is already held, LockNotAcquiredException is thrown

Lock Key

The key attribute identifies the lock. All instances trying to execute the same key compete for the same lock:

// These two methods share the same lock
@DistributedLock(key = "shared-resource")
public void method1() { }

@DistributedLock(key = "shared-resource")
public void method2() { }

// This method has its own independent lock
@DistributedLock(key = "other-resource")
public void method3() { }

Handling Lock Failures

By default, if the lock cannot be acquired, LockNotAcquiredException is thrown:

@Service
public class MyService {

    @DistributedLock(key = "task")
    public void criticalTask() {
        // ...
    }
}

// Calling code
try {
    myService.criticalTask();
} catch (LockNotAcquiredException e) {
    System.out.println("Lock held by another instance: " + e.getLockKey());
    // Handle accordingly - retry later, return cached result, etc.
}

Silent Skip with LockReturnDefaultHandler

For scheduled tasks or fire-and-forget operations, you might want to silently skip if the lock is held:

@Service
public class ScheduledService {

    @Scheduled(cron = "0 0 * * * *")  // Every hour
    @DistributedLock(key = "hourly-job", skipHandler = LockReturnDefaultHandler.class)
    public void hourlyCleanup() {
        // Runs on only one instance
        // Other instances silently skip (return null)
    }
}

LockReturnDefaultHandler returns:

  • null for object types
  • 0 for numeric primitives
  • false for boolean
  • '\0' for char
  • Optional.empty() for Optional

Lease Time (Lock Duration)

Locks automatically expire after the lease time to prevent deadlocks if an instance crashes:

// Default: 10 minutes (from configuration)
@DistributedLock(key = "task1")
public void task1() { }

// Custom: 30 minutes
@DistributedLock(key = "task2", leaseTime = "30m")
public void task2() { }

// Short: 30 seconds
@DistributedLock(key = "task3", leaseTime = "30s")
public void task3() { }

Warning: If your method takes longer than the lease time, the lock expires and another instance can acquire it. See Auto-Renew for long-running tasks.

Method Return Values

Locked methods can return values normally:

@DistributedLock(key = "data-fetch")
public List<User> fetchUsers() {
    return userRepository.findAll();
}

@DistributedLock(key = "calculation")
public int calculate(int a, int b) {
    return a + b;
}

When using LockReturnDefaultHandler and lock acquisition fails:

  • fetchUsers() returns null
  • calculate() returns 0

Exception Handling

Exceptions from your method propagate normally. The lock is always released:

@DistributedLock(key = "risky-operation")
public void riskyOperation() {
    // Lock acquired

    if (somethingWrong) {
        throw new RuntimeException("Error!");
        // Lock is released in finally block
    }

    // Lock released after method completes
}

Aspect Ordering

Locksmith's aspect runs with Ordered.HIGHEST_PRECEDENCE, meaning:

  • Lock is acquired before @Transactional begins
  • Lock is released after @Transactional commits/rollbacks
@DistributedLock(key = "user-update")  // 1. Lock acquired
@Transactional                          // 2. Transaction starts
public void updateUser(User user) {
    userRepository.save(user);          // 3. Database operation
}                                        // 4. Transaction commits
                                         // 5. Lock released

This ensures database changes are committed before another instance can acquire the lock.

Common Patterns

Scheduled Jobs

@Component
public class ScheduledTasks {

    @Scheduled(fixedRate = 60000)  // Every minute
    @DistributedLock(key = "health-check", skipHandler = LockReturnDefaultHandler.class)
    public void healthCheck() {
        // Only one instance runs this check
    }

    @Scheduled(cron = "0 0 2 * * ?")  // Daily at 2 AM
    @DistributedLock(key = "nightly-batch", skipHandler = LockReturnDefaultHandler.class)
    public void nightlyBatch() {
        // Only one instance runs the batch
    }
}

API Endpoint Protection

@RestController
public class PaymentController {

    @PostMapping("/payments/{orderId}")
    @DistributedLock(key = "#{'payment-' + #orderId}")
    public Payment processPayment(@PathVariable String orderId) {
        // Prevents double-processing of the same order
        return paymentService.process(orderId);
    }
}

Resource Access

@Service
public class FileService {

    @DistributedLock(key = "#{'file-' + #filename}")
    public void writeFile(String filename, byte[] content) {
        // Only one instance writes to the file at a time
        Files.write(Path.of(filename), content);
    }
}

Singleton Operations

@Service
public class CacheService {

    @DistributedLock(key = "cache-refresh", skipHandler = LockReturnDefaultHandler.class)
    public void refreshCache() {
        // Only one instance refreshes the cache
        // Others skip if refresh is already in progress
        cache.clear();
        cache.loadAll();
    }
}

Advanced Features

Locksmith provides advanced lock features for complex scenarios:

Feature Description Guide
Dynamic Keys Use SpEL for runtime key resolution Dynamic Keys with SpEL
Lock Types Reentrant, Read, and Write locks Lock Types
Wait Mode Wait for lock instead of failing immediately Lock Acquisition Modes
Auto-Renew Automatic lease extension for long tasks Auto-Renew Lease Time
Lease Detection Detect methods exceeding lease duration Lease Expiration Detection
Skip Handlers Custom logic when lock acquisition fails Skip Handlers

What's Next

Clone this wiki locally