單體應用中 synchronized/ReentrantLock 僅限於單 JVM。微服務、集群部署下,跨進程同步需借助外部共享存儲(Redis、ZooKeeper、etcd、資料庫)。Redis 因高效能、原子操作豐富、部署簡單成為首選。
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end 保證只能刪除自己的鎖,防誤刪。
public interface DistributedLock extends AutoCloseable {
/**
* 獲取鎖(阻塞)
* @param timeout 最大等待時間
* @param unit 時間單位
* @return true 獲取成功,false 超時失敗
* @throws InterruptedException 線程被中斷
*/
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;
/**
* 非阻塞嘗試獲取鎖
* @return true 成功,false 立即失敗
*/
boolean tryLock();
/**
* 釋放鎖
* @throws IllegalMonitorStateException 當前線程不持有鎖
*/
void unlock();
/** 是否由當前線程持有 */
boolean isHeldByCurrentThread();
/** 關閉釋放資源(實現 AutoCloseable 支援 try-with-resources) */
@Override void close();
}
public class RedisDistributedLock implements DistributedLock {
private static final String UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT_OBJ =
new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
private final RedisTemplate<String, String> redisTemplate;
private final String lockKey;
private final long expireMillis;
private final String lockValue; // UUID + ThreadId,唯一標識鎖擁有者
private volatile boolean locked = false;
public RedisDistributedLock(RedisTemplate<String, String> redisTemplate,
String lockKey, long expireMillis) {
this.redisTemplate = redisTemplate;
this.lockKey = "lock:" + lockKey;
this.expireMillis = expireMillis;
this.lockValue = UUID.randomUUID().toString() + ":" + Thread.currentThread().getId();
}
@Override
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
long timeoutNanos = unit.toNanos(timeout);
long start = System.nanoTime();
while (true) {
// 1. 原子獲取鎖
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireMillis, TimeUnit.MILLISECONDS);
if (Boolean.TRUE.equals(acquired)) {
locked = true;
return true;
}
// 2. 計算剩餘等待時間
long elapsed = System.nanoTime() - start;
long remaining = timeoutNanos - elapsed;
if (remaining <= 0) return false;
// 3. 短暫休眠避免佔用 CPU(可優化為 Redis Pub/Sub 通知)
Thread.sleep(Math.min(50, TimeUnit.NANOSECONDS.toMillis(remaining)));
// 4. 檢查中斷
if (Thread.interrupted()) throw new InterruptedException();
}
}
@Override
public boolean tryLock() {
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireMillis, TimeUnit.MILLISECONDS);
locked = Boolean.TRUE.equals(acquired);
return locked;
}
@Override
public void unlock() {
if (!locked) throw new IllegalMonitorStateException("當前線程不持有鎖");
// 執行 Lua 腳本原子釋放
Long result = redisTemplate.execute(UNLOCK_SCRIPT_OBJ,
Collections.singletonList(lockKey), lockValue);
locked = false;
if (result == null || result == 0) {
// 鎖已過期被他人持有,記錄警告
log.warn("鎖釋放失敗,可能已過期轉移: key={}, value={}", lockKey, lockValue);
}
}
@Override
public boolean isHeldByCurrentThread() {
if (!locked) return false;
String current = redisTemplate.opsForValue().get(lockKey);
return lockValue.equals(current);
}
@Override
public void close() {
if (locked) unlock();
}
}
@Service
public class DistributedIdGenerator {
private final RedisTemplate<String, String> redisTemplate;
private final AtomicLong localCounter = new AtomicLong(0);
public DistributedIdGenerator(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public String nextId(String bizTag) {
String lockKey = "id_gen_lock:" + bizTag;
try (DistributedLock lock = new RedisDistributedLock(redisTemplate, lockKey, 5000)) {
if (!lock.tryLock(3, TimeUnit.SECONDS)) {
throw new IllegalStateException("獲取分布式鎖超時: " + bizTag);
}
// 關鍵區段:從 Redis 獲取段號、本地緩存發號
long segment = redisTemplate.opsForValue().increment("id_segment:" + bizTag, 1000);
long start = (segment - 1) * 1000;
return bizTag + "-" + (start + localCounter.incrementAndGet());
}
}
}
手寫鎖教學價值大,生產建議用 Redisson:
// Redisson 使用範例
RLock lock = redisson.getLock("my_lock");
boolean acquired = lock.tryLock(3, 10, TimeUnit.SECONDS); // 等待 3s,自動續期 10s
if (acquired) {
try { /* 關鍵業務 */ }
finally { lock.unlock(); }
}
| 坑點 | 後果 | 解決方案 |
|---|---|---|
| 鎖過期時間 < 業務執行時間 | 鎖自動釋放,並發衝突 | Redisson Watchdog / 預估最大執行時間 × 2 |
| 直接 del 釋放鎖 | 誤刪他人鎖 | Lua 腳本驗證 value 後刪除 |
| 未設定獲取超時 | 死等導致線程耗盡 | 必須使用 tryLock(timeout, unit) |
| 鎖 Key 衝突 | 業務互斥錯誤 | 命名規範:lock:biz:resource:id |
| 單點 Redis 故障 | 鎖服務不可用 | Redis Sentinel/Cluster + Redisson 容錯 |
| 方案 | 一致性 | 效能 | 複雜度 | 適用場景 |
|---|---|---|---|---|
| Redis (SET NX EX) | AP(極少數情況下可能失鎖) | 極高 | 低 | 高併發、允許極低概率失鎖(如緩存重建) |
| Redisson RedLock | CP(多實例多數派) | 中 | 中 | 強一致性要求(如分布式事務協調) |
| ZooKeeper / etcd | CP(強一致) | 中 | 高 | 配置管理、領導者選舉、元數據存儲 |
| 資料庫 FOR UPDATE | CP | 低 | 低 | 低併發、已有資料庫、無額外基建 |