Prerequisites
Ensure your Spring Boot project includes the necessary dependencies for Redis integration. Configure connection parameters in application.yml and inject StringRedisTemplate or RedissonClient.
<!-- Maven Dependency -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- application.yml Configuration -->
spring:
redis:
host: localhost
port: 6379
password: secret_password
database: 0
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 4
max-wait: 1000ms
- Basic Mutex Implementation (Single Node)
Core Concept
The fundamental approach leverages the atomic nature of the Redis command SET key value NX EX seconds. This ensures that a lock is only acquired if the key does not exist, simultaneously setting an expiration time to prevent permanent deadlocks. To avoid deleting another client's lock, a unique identifier (UUID) is stored as the value and verified before release.
Code Structure
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Component
public class SimpleRedisLock {
private final StringRedisTemplate redisTemplate;
public SimpleRedisLock(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* Attempts to acquire a lock.
* @param resourceKey Unique identifier for the resource being locked
* @param ttlSeconds Time-to-live for the lock
* @return A unique token if successful, null otherwise
*/
public String tryAcquire(String resourceKey, long ttlSeconds) {
String token = UUID.randomUUID().toString();
// Atomic operation: Set if Not Exists with Expiration
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(resourceKey, token, ttlSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(acquired) ? token : null;
}
/**
* Releases the lock safely by verifying ownership.
* Note: In high-concurrency production environments, consider Lua scripts
* for atomic check-and-delete operations to eliminate race conditions.
*/
public void release(String resourceKey, String token) {
if (token == null) return;
String currentToken = redisTemplate.opsForValue().get(resourceKey);
if (token.equals(currentToken)) {
redisTemplate.delete(resourceKey);
}
}
}
Usage Guidelines
- Key Naming: Use hierarchical keys like
lock:inventory:item_5001to isolate resources. - Expiration Strategy: Set TTL significantly higher than the maximum expected execution time to account for network latency and GC pauses.
- Finally Block: Always release locks in a
finallyblock to guarantee cleanup even if exceptions occur during business logic execution.
- Advanced Lock with Automatic Renewal (Watchdog Pattern)
The Problem
A static TTL fails when business logic duration is unpredictable. If the process exceeds the TTL, the lock expires prematurely, allowing other threads to enter the critical section, leading to data inconsistency.
The Solution
Implement a background daemon thread that periodically extends the lock's expiration time while the main thread is still executing. The renewal interval is typically set to one-third of the initial TTL.
Implementation Details
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@Component
public class RenewableRedisLock {
private final StringRedisTemplate redisTemplate;
public RenewableRedisLock(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public static class LockHandle {
final String key;
final String token;
final long ttl;
final AtomicBoolean stopRenewal = new AtomicBoolean(false);
public LockHandle(String key, String token, long ttl) {
this.key = key;
this.token = token;
this.ttl = ttl;
}
}
public LockHandle acquire(String key, long ttlSeconds) {
String token = UUID.randomUUID().toString();
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(key, token, ttlSeconds, TimeUnit.SECONDS);
if (!Boolean.TRUE.equals(success)) {
return null;
}
LockHandle handle = new LockHandle(key, token, ttlSeconds);
startRenewalThread(handle);
return handle;
}
private void startRenewalThread(LockHandle handle) {
Thread renewer = new Thread(() -> {
long intervalMs = (handle.ttl * 1000) / 3;
while (!handle.stopRenewal.get()) {
try {
Thread.sleep(intervalMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
// Check if we still own the lock
String currentValue = redisTemplate.opsForValue().get(handle.key);
if (handle.token.equals(currentValue)) {
// Extend expiration
redisTemplate.expire(handle.key, handle.ttl, TimeUnit.SECONDS);
} else {
// Lock lost or expired externally
handle.stopRenewal.set(true);
break;
}
}
});
renewer.setDaemon(true); // Ensure it doesn't prevent JVM shutdown
renewer.start();
}
public void release(LockHandle handle) {
if (handle == null) return;
handle.stopRenewal.set(true); // Signal renewal thread to stop
String currentValue = redisTemplate.opsForValue().get(handle.key);
if (handle.token.equals(currentValue)) {
redisTemplate.delete(handle.key);
}
}
}
- Distributed Locks with Redisson (Cluster Environment)
Why Redisson?
In Redis Cluster or Sentinel architectures, single-node locks are vulnerable to master-slave failover issues (the lock might be lost if the master crashes before syncing to slaves). Redisson implements the Redlock algorithm, providing reliable distributed locking mechanisms out-of-the-box, including automatic watchdog renewal, reentrancy, and fairness policies.
Maven Dependency
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.3</version>
</dependency>
Configuration Example
spring:
redis:
cluster:
nodes:
- redis-node-1:6379
- redis-node-2:6379
- redis-node-3:6379
password: cluster_secret
# Optional Redisson specific tuning
redisson:
threads: 16
netty-threads: 32
Java Integration
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class InventoryService {
private final RedissonClient redissonClient;
public InventoryService(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
public boolean deductStock(Long itemId) {
String lockName = "distributed_lock:stock:" + itemId;
RLock lock = redissonClient.getLock(lockName);
try {
// Try to acquire lock: wait up to 3 seconds, lease time -1 enables auto-renewal (watchdog)
if (lock.tryLock(3, -1, TimeUnit.SECONDS)) {
try {
// Execute critical business logic
performDeduction(itemId);
return true;
} finally {
// Release only if held by current thread
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
} else {
System.out.println("Failed to acquire distributed lock for item: " + itemId);
return false;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
private void performDeduction(Long itemId) {
// Simulate DB update
}
}
Key Advantages
- Auto-Renewal: The built-in watchdog automaticaly extends the lock validity every 10 seconds if the thread is still active, eliminating manual renewal code.
- Safety: Ensures locks are released correctly across multiple nodes, preventing stale locks after node failures.
- Versatility: Supports Fair Locks, Read/Write Locks, and Semaphore patterns beyond simple mutexes.
- Cache Protection: Double-Lock Strategy
Scenario
When handling cache misses (Cache Penetration/Breakdown), simply querying the database under a distributed lock can cause severe contention. A hybrid approach uses a local JVM lock (synchronized) combined with a Redis distributed lock to optimize performance.
Logic Flow
- Check Redis Cache. If hit, return data.
- If miss, acquire a Local Lock (synchronized). Thiss prevents multiple threads within the same JVM instance from hammering Redis for the lock.
- Inside the local lock, check Redis again (Double-Check). If another thread just populated the cache, return immediately.
- If still missing, acquire a Redis Distributed Lock. This coordinates access across different server instances.
- Query Database, populate Redis, release both locks.
Implementation
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.concurrent.TimeUnit;
@Component
public class CachedDataLoader {
private final StringRedisTemplate redisTemplate;
private final RedissonClient redissonClient;
// Global object monitor for local synchronization
private final Object localMonitor = new Object();
public CachedDataLoader(StringRedisTemplate redisTemplate, RedissonClient redissonClient) {
this.redisTemplate = redisTemplate;
this.redissonClient = redissonClient;
}
public String loadProductDetail(Long productId) {
String cacheKey = "product:detail:" + productId;
String lockKey = "lock:product:load:" + productId;
// Step 1: Fast path - Check Cache
String cached = redisTemplate.opsForValue().get(cacheKey);
if (StringUtils.hasText(cached)) {
return cached;
}
// Step 2: Local Lock to reduce Redis contention within the same app instance
synchronized (localMonitor) {
// Step 3: Double Check Cache
cached = redisTemplate.opsForValue().get(cacheKey);
if (StringUtils.hasText(cached)) {
return cached;
}
// Step 4: Distributed Lock for cross-instance coordination
RLock distLock = redissonClient.getLock(lockKey);
try {
// Wait 3s, Auto-renewal enabled (-1)
if (distLock.tryLock(3, -1, TimeUnit.SECONDS)) {
try {
// Final check inside distributed lock (in case another node updated it)
cached = redisTemplate.opsForValue().get(cacheKey);
if (StringUtils.hasText(cached)) {
return cached;
}
// Step 5: Load from DB
String dbData = fetchFromDatabase(productId);
// Step 6: Populate Cache with TTL to prevent avalanche
if (dbData != null) {
redisTemplate.opsForValue().set(cacheKey, dbData, 300, TimeUnit.SECONDS);
}
return dbData;
} finally {
if (distLock.isHeldByCurrentThread()) {
distLock.unlock();
}
}
} else {
// Handle lock acquisition failure gracefully
throw new RuntimeException("System busy, please retry");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while loading data", e);
}
}
}
private String fetchFromDatabase(Long id) {
// DAO call simulation
return "Product Info for ID: " + id;
}
}
- Production Best Practices
- Connection Pooling: Always configure Lettuce or Jedis connection pools appropriately. Default settings often lead to bottlenecks under high load.
- Granularity: Avoid coarse-grained locks (e.g., locking all products). Lock specific resource IDs (e.g.,
lock:product:101) to maximize concurrency. - Error Handling: Wrap lock acquisition and release in try-catch blocks. Log failures explicitly to aid in debugging race conditions.
- Testing: Conduct stress tests simulating high concurrency and node failures to verify lock reliability and recovery mechanisms.
- Timeout Management: Balance
waitTime(how long to wait for a lock) andleaseTime(how long the lock is held). Too short causes frequent failures; too long reduces throughput.