-
Notifications
You must be signed in to change notification settings - Fork 0
Distributed Locks
This guide covers the fundamentals of using @DistributedLock for exclusive access control across multiple servers.
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 │
└─────────┘ └─────────┘ └─────────┘
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:
- Locksmith tries to acquire lock
lock:my-taskin Redis - If successful, the method executes
- After completion (or exception), the lock is released
- If the lock is already held,
LockNotAcquiredExceptionis thrown
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() { }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.
}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:
-
nullfor object types -
0for numeric primitives -
falsefor boolean -
'\0'for char -
Optional.empty()for Optional
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.
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()returnsnull -
calculate()returns0
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
}Locksmith's aspect runs with Ordered.HIGHEST_PRECEDENCE, meaning:
- Lock is acquired before
@Transactionalbegins - Lock is released after
@Transactionalcommits/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 releasedThis ensures database changes are committed before another instance can acquire the lock.
@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
}
}@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);
}
}@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);
}
}@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();
}
}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 |
- Dynamic Keys with SpEL - Use method parameters in lock keys
- Lock Acquisition Modes - Wait for lock instead of failing immediately
- Skip Handlers - Custom logic when lock acquisition fails
