锁是在并发编程中控制多个线程对一个变量的访问顺序的一种同步机制
拿到锁的线程,才能进入 "房间" 操作资源;
没拿到锁的线程,要么排队等待,要么放弃操作,直到锁被释放。
一、分布式锁的演进路线
┌─────────────────────────────────────────────────────────────────────────┐
│ 分布式锁演进路线 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 单机锁 ──────► 单Redis锁 ──────► Redlock ──────► Redisson │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ synchronized SETNX+EX 多节点投票 看门狗+Redlock │
│ ReentrantLock 原子操作 N/2+1多数派 自动续期+高可用 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
二、单机 Redis 锁的问题
2.1 基础实现
/**
* 单机Redis分布式锁 - 基础实现
*/
public class SimpleRedisLock {
private StringRedisTemplate redisTemplate;
private String lockKey;
private String lockValue;
private long expireTime;
/**
* 加锁 - 使用SET NX EX原子命令
*/
public boolean lock() {
// 生成唯一标识,防止误删其他线程的锁
this.lockValue = UUID.randomUUID().toString() + ":" + Thread.currentThread().getId();
Boolean success = redisTemplate.opsForValue().setIfAbsent(
lockKey,
lockValue,
expireTime,
TimeUnit.MILLISECONDS
);
return Boolean.TRUE.equals(success);
}
/**
* 解锁 - 必须使用Lua脚本保证原子性
* 上锁 判断 更新
*/
public boolean unlock() {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
lockValue
);
return Long.valueOf(1).equals(result);
}
}
2.2 单机锁存在的核心问题
锁到时间自动过期释放 ,但业务并未完成
主从复制时锁丢失
线程崩溃,锁永久保留不被释放
┌─────────────────────────────────────────────────────────────────────────┐
│ 问题1: 锁过期但业务未完成 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 时间轴: │
│ ────────────────────────────────────────────────────────────────► │
│ │
│ 客户端A: [获取锁]═══════════════════════════════════════════[业务完成]│
│ ▲ ▲ │
│ │ │ │
│ │ 锁已过期(10s) │
│ │ │ │
│ 客户端B: │ [获取锁]═════════[释放锁] │
│ │ ▲ │
│ │ │ │
│ 问题: │ A的业务还在执行,B也获取到了锁! │
│ │ 互斥性被破坏! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ 问题2: 主从异步复制导致锁丢失 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 客户端A │
│ │ │
│ │ 1. SET lock:order "uuid" NX EX 10 │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Master │ ──── 异步复制 ───── │ Slave │ │
│ │ (有锁) │ (还没同步) │ (无锁) │ │
│ └──────────┘ └──────────┘ │
│ │ │ │
│ │ 2. Master宕机 │ 3. Slave晋升为Master │
│ ▼ ▼ │
│ [GG] ┌──────────┐ │
│ │New Master│ │
│ 客户端B │ (无锁) │ │
│ │ └──────────┘ │
│ │ 4. SET lock:order "uuid2" NX EX 10 │
│ ▼ │ │
│ [成功获取锁!] ▼ │
│ │
│ ⚠️ A和B同时持有锁,分布式锁失效! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
三、Redlock 算法详解
3.1 Redlock 核心思想
┌─────────────────────────────────────────────────────────────────────────┐
│ Redlock 核心架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 客户端 │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Redis1 │ │ Redis2 │ │ Redis3 │ ... (共N个独立节点) │
│ │ (独立) │ │ (独立) │ │ (独立) │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 成功 ✓ 成功 ✓ 失败 ✗ │
│ │
│ 获取锁成功条件: │
│ 1. 在超过半数(N/2+1)的节点上成功获取锁 │
│ 2. 获取锁的总耗时 < 锁的有效时间 │
│ │
│ 实际锁有效时间 = 锁TTL - 获取锁耗时 - 时钟漂移补偿 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
3.2 Redlock 详细流程
/**
* Redlock算法实现
*/
public class RedlockAlgorithm {
// 5个独立的Redis节点(推荐奇数个)
private List<RedisConnection> redisNodes;
// 时钟漂移因子
private static final double CLOCK_DRIFT_FACTOR = 0.01;
// 重试延迟
private static final int RETRY_DELAY_MS = 200;
/**
* Redlock加锁核心算法
*/
public LockResult lock(String resource, long ttlMs) {
String lockValue = generateUniqueValue();
int quorum = redisNodes.size() / 2 + 1; // 多数派 = N/2 + 1
int retryCount = 3;
while (retryCount-- > 0) {
int successCount = 0;
long startTime = System.currentTimeMillis();
// Step 1: 依次向所有节点请求加锁
for (RedisConnection node : redisNodes) {
try {
boolean acquired = tryLockOnNode(
node, resource, lockValue, ttlMs
);
if (acquired) {
successCount++;
}
} catch (Exception e) {
// 节点不可用,继续尝试其他节点
log.warn("Node {} unavailable", node.getId());
}
}
// Step 2: 计算获取锁消耗的时间
long elapsedTime = System.currentTimeMillis() - startTime;
// Step 3: 计算锁的实际有效时间
long clockDrift = (long) (ttlMs * CLOCK_DRIFT_FACTOR) + 2;
long validityTime = ttlMs - elapsedTime - clockDrift;
// Step 4: 判断是否获取锁成功
if (successCount >= quorum && validityTime > 0) {
return new LockResult(true, lockValue, validityTime);
}
// Step 5: 获取失败,释放所有已获取的锁
unlockAll(resource, lockValue);
// Step 6: 随机延迟后重试,避免活锁
sleepRandom(RETRY_DELAY_MS);
}
return new LockResult(false, null, 0);
}
/**
* 在单个节点上尝试加锁
*/
private boolean tryLockOnNode(RedisConnection node,
String resource,
String value,
long ttlMs) {
// SET resource value NX PX ttl
String result = node.set(resource, value, "NX", "PX", ttlMs);
return "OK".equals(result);
}
/**
* 释放所有节点上的锁
*/
public void unlockAll(String resource, String value) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
for (RedisConnection node : redisNodes) {
try {
node.eval(script, 1, resource, value);
} catch (Exception e) {
// 忽略异常,尽力释放
}
}
}
private String generateUniqueValue() {
return UUID.randomUUID().toString() + ":" +
Thread.currentThread().getId() + ":" +
System.currentTimeMillis();
}
}
3.3 Redlock 时序图
┌─────────────────────────────────────────────────────────────────────────────┐
│ Redlock 完整时序图 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Client Redis1 Redis2 Redis3 Redis4 Redis5 │
│ │ │ │ │ │ │ │
│ │ T1: 记录开始时间 │
│ ├───────────────►│ │ │ │ │ │
│ │ SET key uuid NX PX 10000 │ │ │ │ │
│ │◄───────────────┤ │ │ │ │ │
│ │ OK ✓ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ ├────────────────────────────►│ │ │ │ │
│ │ SET key uuid NX PX 10000 │ │ │ │
│ │◄────────────────────────────┤ │ │ │ │
│ │ OK ✓ │ │ │ │ │
│ │ │ │ │ │ │ │
│ ├─────────────────────────────────────────►│ │ │ │
│ │ SET key uuid NX PX 10000 │ │ │
│ │◄─────────────────────────────────────────┤ │ │ │
│ │ OK ✓ │ │ │ │ │
│ │ │ │ │ │ │ │
│ ├───────────────────────────────────────────────────────►│ │ │
│ │ SET key uuid NX PX 10000 │ │ │
│ │◄─X─────────────────────────────────────────────────────┤ │ │
│ │ 超时/失败 ✗ │ │ │ │
│ │ │ │ │ │ │ │
│ ├─────────────────────────────────────────────────────────────────►│ │
│ │ SET key uuid NX PX 10000 │ │
│ │◄─X───────────────────────────────────────────────────────────────┤ │
│ │ 超时/失败 ✗ │ │ │ │
│ │ │ │ │ │ │ │
│ │ T2: 计算耗时 │
│ │ elapsed = T2 - T1 = 150ms │
│ │ │
│ │ 判断结果: │
│ │ ✓ 成功节点数: 3 >= 3 (N/2+1 = 5/2+1 = 3) │
│ │ ✓ 有效时间: 10000 - 150 - 100(漂移) = 9750ms > 0 │
│ │ │
│ │ ═══════════════════════════════════════ │
│ │ │ 加锁成功!开始执行业务 │ │
│ │ ═══════════════════════════════════════ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
3.4 Redlock 的争议 (Martin Kleppmann vs Antirez)
┌─────────────────────────────────────────────────────────────────────────────┐
│ Redlock 著名争论 │
├────────────────────────────────┬────────────────────────────────────────────┤
│ Martin Kleppmann (反对方) │ Antirez - Redis作者 (支持方) │
├────────────────────────────────┼────────────────────────────────────────────┤
│ │ │
│ 问题1: GC暂停导致锁失效 │ 回应: 这是所有分布式锁的通病 │
│ ┌─────────────────────────┐ │ 建议使用fencing token解决 │
│ │ Client获取锁 │ │ │
│ │ ↓ │ │ │
│ │ [Full GC暂停30秒] │ │ │
│ │ ↓ │ │ │
│ │ 锁已过期,但Client不知 │ │ │
│ │ Client继续操作共享资源! │ │ │
│ └─────────────────────────┘ │ │
│ │ │
├────────────────────────────────┼────────────────────────────────────────────┤
│ │ │
│ 问题2: 时钟跳跃问题 │ 回应: 现代系统时钟足够稳定 │
│ ┌─────────────────────────┐ │ - 使用NTP逐步调整而非跳跃 │
│ │ 节点A时钟突然向前跳5分钟 │ │ - 配置合理的maxSyncDrift │
│ │ 导致锁提前过期 │ │ │
│ └─────────────────────────┘ │ │
│ │ │
├────────────────────────────────┼────────────────────────────────────────────┤
│ │ │
│ 问题3: 网络延迟问题 │ 回应: validity_time已考虑这个因素 │
│ 获取锁成功的消息延迟到达时 │ 如果validity_time<=0会认为失败 │
│ 锁可能已经过期 │ │
│ │ │
├────────────────────────────────┴────────────────────────────────────────────┤
│ │
│ 💡 结论:Redlock适用于对一致性要求不是极端严格的场景 │
│ 如果需要100%的安全性,请使用支持共识算法的系统(如ZooKeeper, etcd) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
四、看门狗机制 (Watchdog)
4.1 看门狗核心思想
┌─────────────────────────────────────────────────────────────────────────────┐
│ 看门狗机制原理 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ │
│ 业务线程 看门狗线程 │
│ │ │ │
│ ┌────┴────┐ ┌─────┴─────┐ │
│ │ 获取锁 │ │ 定时检查 │ │
│ │ TTL=30s │◄──── 启动 ──────── │ 间隔=10s │ │
│ └────┬────┘ └─────┬─────┘ │
│ │ │ │
│ │ 执行业务... │ │
│ │ │ │ 检查锁是否还持有 │
│ │ │ ◄──────────────────────┤ 如果是,续期到30s │
│ │ │ │ │
│ │ │ 10s后... │ │
│ │ │ ◄──────────────────────┤ 再次续期 │
│ │ │ │ │
│ │ │ 20s后... │ │
│ │ │ ◄──────────────────────┤ 再次续期 │
│ │ │ │ │
│ │ 业务完成 │ │
│ ┌────┴────┐ │ │
│ │ 释放锁 │────── 停止 ─────────────►│ │
│ └─────────┘ │ │
│ │
│ 优势: │
│ 1. 业务执行时间不确定时,自动续期防止锁过期 │
│ 2. 客户端崩溃时,看门狗停止,锁自然过期释放 │
│ 3. 避免手动设置过长的锁超时时间 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
4.2 Redisson 看门狗实现源码分析
/**
* Redisson 看门狗机制核心实现(简化版)
*/
public class RedissonLockWatchdog {
// 默认锁超时时间:30秒
private static final long LOCK_WATCHDOG_TIMEOUT = 30 * 1000;
// 续期任务Map,Key是锁名,Value是续期任务
private final ConcurrentMap<String, ExpirationEntry> expirationRenewalMap
= new ConcurrentHashMap<>();
/**
* 加锁核心方法
*/
public void lock(String lockName) {
long threadId = Thread.currentThread().getId();
// 尝试加锁(使用Lua脚本保证原子性)
Long ttl = tryAcquire(lockName, LOCK_WATCHDOG_TIMEOUT, threadId);
if (ttl == null) {
// 加锁成功,启动看门狗
scheduleExpirationRenewal(lockName, threadId);
return;
}
// 加锁失败,订阅锁释放事件,等待重试...
// (省略等待逻辑)
}
/**
* 尝试加锁的Lua脚本
*/
private Long tryAcquire(String lockName, long leaseTime, long threadId) {
String script =
// 如果锁不存在
"if (redis.call('exists', KEYS[1]) == 0) then " +
// 创建锁,并设置重入计数为1
" redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
// 设置过期时间
" redis.call('pexpire', KEYS[1], ARGV[1]); " +
" return nil; " +
"end; " +
// 如果锁存在且是当前线程持有(可重入)
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
// 重入计数+1
" redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
// 重置过期时间
" redis.call('pexpire', KEYS[1], ARGV[1]); " +
" return nil; " +
"end; " +
// 锁被其他线程持有,返回剩余过期时间
"return redis.call('pttl', KEYS[1]);";
return redisClient.eval(script,
Collections.singletonList(lockName),
leaseTime,
getLockName(threadId)
);
}
/**
* 🐕 核心:调度看门狗续期任务
*/
private void scheduleExpirationRenewal(String lockName, long threadId) {
ExpirationEntry entry = new ExpirationEntry();
// 如果已经有续期任务在运行,直接返回
ExpirationEntry oldEntry = expirationRenewalMap.putIfAbsent(
getEntryName(lockName), entry
);
if (oldEntry != null) {
// 重入锁,复用已有的续期任务
oldEntry.addThreadId(threadId);
} else {
// 首次加锁,启动新的续期任务
entry.addThreadId(threadId);
renewExpiration(lockName, entry);
}
}
/**
* 🐕 核心:执行续期
*/
private void renewExpiration(String lockName, ExpirationEntry entry) {
// 创建定时任务,每 lockWatchdogTimeout/3 执行一次(即10秒)
Timeout task = commandExecutor.getConnectionManager()
.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
// 检查是否还持有锁
ExpirationEntry ent = expirationRenewalMap.get(getEntryName(lockName));
if (ent == null) {
// 锁已释放,停止续期
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
// 执行续期
CompletionStage<Boolean> future = renewExpirationAsync(
lockName, threadId
);
future.whenComplete((result, e) -> {
if (e != null) {
log.error("Can't update lock expiration", e);
expirationRenewalMap.remove(getEntryName(lockName));
return;
}
if (result) {
// 续期成功,调度下一次续期
renewExpiration(lockName, entry);
} else {
// 锁已不存在,取消续期
cancelExpirationRenewal(lockName);
}
});
}
},
LOCK_WATCHDOG_TIMEOUT / 3, // 每10秒执行一次
TimeUnit.MILLISECONDS
);
entry.setTimeout(task);
}
/**
* 续期的Lua脚本
*/
private CompletionStage<Boolean> renewExpirationAsync(String lockName, long threadId) {
String script =
// 检查锁是否还被当前线程持有
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
// 重置过期时间
" redis.call('pexpire', KEYS[1], ARGV[1]); " +
" return 1; " +
"end; " +
"return 0;";
return redisClient.evalAsync(script,
Collections.singletonList(lockName),
LOCK_WATCHDOG_TIMEOUT,
getLockName(threadId)
);
}
/**
* 释放锁
*/
public void unlock(String lockName) {
long threadId = Thread.currentThread().getId();
// 执行解锁
Boolean result = unlockInner(lockName, threadId);
// 取消看门狗
cancelExpirationRenewal(lockName);
}
/**
* 取消续期任务
*/
private void cancelExpirationRenewal(String lockName) {
ExpirationEntry entry = expirationRenewalMap.remove(getEntryName(lockName));
if (entry != null) {
entry.getTimeout().cancel();
}
}
}
4.3 看门狗时序图
┌─────────────────────────────────────────────────────────────────────────────┐
│ 看门狗续期时序图 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 时间(秒) 0 10 20 30 40 50 60 70 │
│ │ │ │ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ │
│ │
│ 业务线程 ╔══════════════════════════════════════════════════╗ │
│ ║ 执行业务逻辑 ║ │
│ ╚══════════════════════════════════════════════════╝ │
│ │ │ │
│ 加锁 释放锁 │
│ │ │ │
│ ▼ ▼ │
│ │
│ 锁TTL 30s │
│ │═══════════════════════════════════════════════════│ │
│ ▲ ▲ ▲ ▲ ▲ │
│ │ │ │ │ │ │
│ 设置TTL 续期至30s 续期至30s 续期至30s 续期至30s │
│ │ │ │ │ │ │
│ 看门狗 │ │ │ │ │ │
│ 任务 ├─────────┼─────────┼─────────┼─────────┤ │
│ │ │ │ │ │ │
│ T=0 T=10s T=20s T=30s T=40s │
│ │
│ ═══════════════════════════════════════════════════════════════════════ │
│ │
│ 如果没有看门狗: │
│ 锁TTL 30s │
│ │════════════════════════════════════│ │
│ │ │
│ 锁过期! │
│ │ │
│ 业务还在执行... ▼ │
│ 其他客户端可以获取锁! │
│ ⚠️ 互斥性被破坏! │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
4.4 Redisson 完整使用示例
/**
* Redisson分布式锁完整使用示例
*/
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient;
/**
* 方式1: 自动看门狗(推荐)
* 不指定leaseTime,默认30秒,自动续期
*/
public void createOrderWithWatchdog(String orderId) {
RLock lock = redissonClient.getLock("lock:order:" + orderId);
try {
// 最多等待10秒获取锁,不指定leaseTime则启用看门狗
boolean acquired = lock.tryLock(10, TimeUnit.SECONDS);
if (acquired) {
try {
// 执行业务逻辑,无论多久都不会锁过期
processOrder(orderId);
} finally {
lock.unlock();
}
} else {
throw new RuntimeException("获取锁失败");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* 方式2: 指定锁持有时间(不启用看门狗)
* 适用于能预估业务时间的场景
*/
public void createOrderWithFixedTime(String orderId) {
RLock lock = redissonClient.getLock("lock:order:" + orderId);
try {
// 指定leaseTime=30秒,不会启用看门狗
boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (acquired) {
try {
processOrder(orderId); // 必须在30秒内完成
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* 方式3: 可重入锁
*/
public void nestedLock(String resourceId) {
RLock lock = redissonClient.getLock("lock:resource:" + resourceId);
lock.lock();
try {
System.out.println("第一次获取锁");
// 同一线程再次获取同一把锁(可重入)
lock.lock();
try {
System.out.println("第二次获取锁(重入)");
} finally {
lock.unlock(); // 重入计数-1
}
} finally {
lock.unlock(); // 重入计数-1,此时为0,真正释放锁
}
}
/**
* 方式4: RedLock(多节点)
*/
public void createOrderWithRedlock(String orderId) {
// 获取多个独立Redis节点的锁
RLock lock1 = redissonClient1.getLock("lock:order:" + orderId);
RLock lock2 = redissonClient2.getLock("lock:order:" + orderId);
RLock lock3 = redissonClient3.getLock("lock:order:" + orderId);
// 创建RedLock
RLock redLock = redissonClient.getRedLock(lock1, lock2, lock3);
try {
boolean acquired = redLock.tryLock(10, TimeUnit.SECONDS);
if (acquired) {
try {
processOrder(orderId);
} finally {
redLock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* 方式5: 公平锁
*/
public void fairLockExample(String resourceId) {
RLock fairLock = redissonClient.getFairLock("lock:fair:" + resourceId);
fairLock.lock();
try {
// 按请求顺序获取锁
processResource(resourceId);
} finally {
fairLock.unlock();
}
}
/**
* 方式6: 读写锁
*/
public void readWriteLockExample(String dataId) {
RReadWriteLock rwLock = redissonClient.getReadWriteLock("lock:data:" + dataId);
// 读锁 - 允许多个读操作并发
RLock readLock = rwLock.readLock();
readLock.lock();
try {
readData(dataId);
} finally {
readLock.unlock();
}
// 写锁 - 独占
RLock writeLock = rwLock.writeLock();
writeLock.lock();
try {
writeData(dataId);
} finally {
writeLock.unlock();
}
}
}
五、Redis 锁的数据结构
5.1 可重入锁的 Hash 结构
┌─────────────────────────────────────────────────────────────────────────────┐
│ Redisson可重入锁的Redis数据结构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 使用 Hash 结构存储锁信息: │
│ │
│ Key: lock:order:12345 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Hash │ │
│ ├─────────────────────────────────┬───────────────────────────────────┤ │
│ │ Field │ Value │ │
│ ├─────────────────────────────────┼───────────────────────────────────┤ │
│ │ UUID:threadId │ 重入次数 │ │
│ │ 5a3f...bc4d:1 │ 3 │ │
│ └─────────────────────────────────┴───────────────────────────────────┘ │
│ │
│ 示例: │
│ > HGETALL lock:order:12345 │
│ 1) "5a3f4e2b-1c7d-4a8e-9f0b-2c3d4e5f6a7b:1" // 客户端UUID:线程ID │
│ 2) "3" // 重入了3次 │
│ │
│ 优势: │
│ 1. 支持可重入:通过计数器实现 │
│ 2. 防止误删:Field包含客户端标识 │
│ 3. 原子操作:使用Lua脚本操作Hash │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5.2 公平锁的数据结构
┌─────────────────────────────────────────────────────────────────────────────┐
│ Redisson公平锁的Redis数据结构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 锁本身(Hash) │
│ Key: lock:fair:resource │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Field: "UUID:threadId" │ Value: 重入次数 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 2. 等待队列(List) │
│ Key: redisson_lock_queue:{lock:fair:resource} │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ [UUID:threadId-1] → [UUID:threadId-2] → [UUID:threadId-3] │ │
│ │ ▲ ▲ ▲ │ │
│ │ 队首(下一个) 等待中 等待中 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 3. 超时时间集合(Sorted Set) │
│ Key: redisson_lock_timeout:{lock:fair:resource} │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Score: 超时时间戳 │ │
│ │ Member: UUID:threadId │ │
│ │ │ │
│ │ ┌──────────────────┬────────────────────────┐ │ │
│ │ │ Score │ Member │ │ │
│ │ ├──────────────────┼────────────────────────┤ │ │
│ │ │ 1699856400000 │ uuid1:1 │ │ │
│ │ │ 1699856400500 │ uuid2:1 │ │ │
│ │ │ 1699856401000 │ uuid3:1 │ │ │
│ │ └──────────────────┴────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 公平锁工作流程: │
│ 1. 新请求加入队列尾部 │
│ 2. 检查自己是否在队首 │
│ 3. 如果是队首且锁空闲,获取锁 │
│ 4. 如果不是队首,等待前面的请求超时或完成 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
六、完整解决方案对比
┌──────────────────────────────────────────────────────────────────────────────┐
│ 分布式锁方案对比 │
├────────────┬────────────────┬─────────────────┬──────────────────────────────┤
│ 方案 │ 优点 │ 缺点 │ 适用场景 │
├────────────┼────────────────┼─────────────────┼──────────────────────────────┤
│ │ • 实现简单 │ • 主从切换锁丢失│ │
│ 单机Redis │ • 性能高 │ • 无自动续期 │ 单机/主从环境 │
│ SETNX+EX │ • 原子操作 │ • 不支持重入 │ 对一致性要求不高的场景 │
│ │ │ • 锁过期风险 │ │
├────────────┼────────────────┼─────────────────┼──────────────────────────────┤
│ │ • 更高可用性 │ • 实现复杂 │ │
│ Redlock │ • 多节点容错 │ • 存在争议 │ 需要更高可用性 │
│ │ • 解决主从问题 │ • 性能略低 │ 可接受理论上的小概率问题 │
│ │ │ • 需多节点部署 │ │
├────────────┼────────────────┼─────────────────┼──────────────────────────────┤
│ │ • 自动续期 │ • 依赖Redisson │ │
│ Redisson │ • 可重入 │ • 相对重量级 │ 生产环境首选 │
│ 看门狗 │ • 支持多种锁 │ │ 业务时间不确定的场景 │
│ │ • 使用简单 │ │ │
├────────────┼────────────────┼─────────────────┼──────────────────────────────┤
│ │ • 强一致性 │ • 性能较低 │ │
│ ZooKeeper │ • 天然顺序性 │ • 部署复杂 │ 金融级一致性要求 │
│ │ • 临时节点自动 │ • 重量级 │ 对数据安全要求极高的场景 │
│ │ 释放 │ │ │
├────────────┼────────────────┼─────────────────┼──────────────────────────────┤
│ │ • 强一致性 │ • 需要独立部署 │ │
│ etcd │ • Raft共识 │ • 运维复杂度 │ 云原生环境 │
│ │ • 租约机制 │ │ Kubernetes生态 │
│ │ • 高性能 │ │ │
└────────────┴────────────────┴─────────────────┴──────────────────────────────┘
七、最佳实践总结
/**
* 分布式锁最佳实践
*/
public class DistributedLockBestPractice {
/**
* 实践1: 锁的key设计
*/
public String generateLockKey(String businessType, String resourceId) {
// 格式:lock:{业务类型}:{资源ID}
return String.format("lock:%s:%s", businessType, resourceId);
// 例:lock:order:12345
// 例:lock:inventory:SKU001
}
/**
* 实践2: 锁的value设计
*/
public String generateLockValue() {
// 包含:机器标识 + 进程ID + 线程ID + UUID
return String.format("%s:%d:%d:%s",
getHostName(),
ProcessHandle.current().pid(),
Thread.currentThread().getId(),
UUID.randomUUID().toString()
);
// 便于问题排查
}
/**
* 实践3: 锁超时时间设置
*/
public void lockTimeoutStrategy() {
// 方案A: 使用看门狗(推荐)
// - 不指定leaseTime
// - 默认30秒,自动续期
RLock lock = redisson.getLock("myLock");
lock.lock(); // 启用看门狗
// 方案B: 预估业务时间
// - 预估时间 × 2~3倍作为超时时间
// - 适用于业务时间可预估的场景
lock.lock(60, TimeUnit.SECONDS); // 不启用看门狗
}
/**
* 实践4: 异常处理
*/
public void handleLockException(String resourceId) {
RLock lock = redisson.getLock("lock:resource:" + resourceId);
try {
if (!lock.tryLock(10, TimeUnit.SECONDS)) {
// 获取锁超时
throw new BusinessException("系统繁忙,请稍后重试");
}
try {
doBusiness();
} finally {
// 只有锁持有者才能释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException("操作被中断");
}
}
/**
* 实践5: 锁粒度控制
*/
public void lockGranularity() {
// ❌ 粗粒度锁 - 锁整个服务
RLock badLock = redisson.getLock("lock:order-service");
// ✅ 细粒度锁 - 锁具体资源
RLock goodLock = redisson.getLock("lock:order:" + orderId);
// ✅ 更细粒度 - 锁具体操作
RLock finestLock = redisson.getLock("lock:order:pay:" + orderId);
}
}
八、面试常见问题速查
|-----------------------|----------------------------------------------------------------------------------|
| 问题 | 答案要点 |
| Redis锁如何防止误删? | Value存UUID+线程ID,解锁用Lua脚本原子验证 |
| 主从切换锁丢失怎么办? | 使用Redlock多节点投票,或使用ZK/etcd |
| 锁过期业务没执行完? | 使用看门狗自动续期(Redisson默认每10秒续期到30秒) |
| 看门狗何时停止? | 1) 主动unlock 2) 客户端崩溃 3) 锁被其他客户端获取 |
| Redlock为什么要N/2+1? | 多数派原则,保证同一时刻只有一个客户端获取锁成功 |
| Redlock 为什么不安全 | Redis 集群在分布式理论中是实现了 AP,高可用(available),如果要实现一致性 CP 中的 (consistency)优先采用 zookeeper |
| Redlock的时钟漂移? | 算法已考虑,validity_time扣除了漂移补偿 |
| 可重入锁原理? | Hash结构存储锁信息,Field记录持有者,Value记录重入次数 |
| 公平锁如何实现? | List作为等待队列,Sorted Set管理超时 |