Redis 分布式锁:手动实现原理与存在的问题
一、Redis 分布式锁的实现原理
Redis 分布式锁主要利用 SET NX EX 实现,核心是保证多个线程或进程之间的互斥访问。
手动实现时,可以定义一个 ILock 接口,封装获取锁和释放锁的方法。
1. 获取锁
使用 Redis 的 SET 命令:
SET lock:order:1001 uuid:threadId NX EX 30参数说明:
Spring Boot 中可以使用:
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);其中:
lockKey:业务锁 Key,例如lock:order:1001lockValue:唯一标识,例如UUID + 线程ID
注意:不能只使用线程 ID,因为不同 JVM 中可能存在相同的线程 ID。推荐使用全局唯一标识。
2. 释放锁
释放锁需要考虑两个问题:
问题一:不能误删其他线程的锁。
例如:
线程 A 获取锁,TTL 为 30 秒。
线程 A 业务执行时间过长,锁自动过期。
线程 B 获取了同一把锁。
线程 A 执行完成,如果直接执行 DEL,就会误删线程 B 的锁。
解决方案:释放锁时,必须比较 Redis 中的锁标识与当前线程持有的锁标识是否一致,只有一致才能删除。
问题二:判断锁归属和删除锁必须具有原子性。
如果分开执行 GET 和 DEL,两个命令之间可能发生锁过期和其他线程获取锁的情况。
因此,需要使用 Lua 脚本:
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
endRedis 执行该 Lua 脚本时,可以保证判断和删除操作的原子性。
二、手动实现分布式锁的主要问题
1. 不可重入
使用简单的 SET NX 实现的锁默认不支持重入。
例如:
线程 A 已经获取锁。
线程 A 再次申请同一把锁。
由于 Key 已经存在,第二次获取失败。
解决方案:记录锁的持有者和重入次数。Redisson 使用 Redis Hash 结构实现可重入锁。
2. 不支持自动重试
简单实现中,获取锁失败后通常直接返回失败。
解决方案:增加重试机制,例如循环尝试、等待间隔、最大等待时间等。
注意:SET NX 本身并非不能重试,而是简单封装通常没有实现重试机制。
3. 锁超时释放问题
当业务执行时间超过锁的 TTL 时:
线程 A 获取锁。
线程 A 因业务阻塞或 Full GC 暂停执行。
锁 TTL 到期,Redis 自动释放锁。
线程 B 获取锁,开始执行业务。
线程 A 恢复执行,导致两个线程可能同时操作相同数据。
问题本质:锁提前过期,破坏业务互斥性。
解决方案:
使用 Redisson 看门狗机制自动续期。
对强一致性要求较高的业务,结合 Fencing Token、数据库乐观锁或条件更新等机制。
注意:看门狗不能完全避免超长时间 Full GC 导致的锁过期。
4. Redis 主从一致性问题
Redis 主从复制通常是异步的,可能出现锁丢失问题。
例如:
线程 A 在 Redis 主节点成功获取锁。
主节点还没将锁同步给从节点,就发生宕机。
从节点升级为新的主节点。
新主节点不存在之前的锁。
线程 B 成功获取锁,造成两个线程都认为自己拥有锁。
问题本质:Redis 异步复制可能造成锁状态丢失。
Redisson 的可重入、重试和看门狗机制,都不能从根本上消除普通 Redis 主从故障切换带来的锁丢失风险。
对于强一致性要求较高的业务,需要采用额外的数据一致性保护机制。
三、释放锁失败能否通过事务回滚解决?
不能完全解决。
假设线程 A 发生 Full GC,恢复执行时锁已经被线程 B 占用。
即使线程 A 执行业务完成后,在释放锁时发现锁标识不一致,并抛出异常,也不能保证业务一定回滚。
原因包括:
数据库事务可能已经提交。
MQ 消息可能已经发送。
第三方接口可能已经调用。
即使数据库事务成功回滚,也不能保证其他外部操作同步回滚。
因此,Lua 脚本只能保证安全释放锁,不能保证锁过期后的业务互斥性。
对于需要严格防止过期锁持有者继续写入的场景,可以使用 Fencing Token,并要求数据库或下游系统原子地校验令牌、拒绝旧令牌。令牌生成机制本身也必须可靠。
四、手动实现与 Redisson 对比
五、总结
手动实现 Redis 分布式锁,需要记住以下几个核心知识点:
获取锁: 使用
SET NX EX,保证加锁与设置过期时间的原子性。唯一标识: 使用 UUID 等唯一标识区分锁的持有者,不能只依赖线程 ID。
安全释放: 使用 Lua 脚本原子地判断锁归属并删除,避免误删其他线程的锁。
不可重入: 简单 SET NX 锁不支持重入,需要维护锁的持有者和重入次数。
不可自动重试: 简单实现通常没有等待重试机制,需要额外开发。
超时释放: TTL 过期可能破坏业务互斥性,看门狗可以缓解,但不是绝对保证。
主从一致性: Redis 异步复制可能导致锁丢失,需要根据业务一致性要求采取额外措施。
实际开发建议: Java 8 + Spring Boot 项目中,优先使用 Redisson,而不是自己实现完整的分布式锁。对于资金扣减、库存扣减等重要业务,不能只依赖分布式锁,还需要结合数据库约束、条件更新或其他一致性保护手段。
