[Redis小技巧32]Redis分布式锁的至暗时刻:从原理演进到时钟跳跃的终极博弈

一、演进之路:从SETNX到Redisson

分布式锁的实现并非一蹴而就,它经历了一个由简入繁、由脆弱到健壮的过程。

  1. 青铜时代:SETNX + EXPIRE

    早期的做法是利用 SETNX (SET if Not eXists) 命令。

    bash 复制代码
    SETNX lock_key client_id
    EXPIRE lock_key 30

    致命缺陷 :这两条命令不具备原子性。如果 SETNX 成功但服务在 EXPIRE 执行前宕机,锁将永远无法释放(死锁)。

  2. 白银时代:原子性SET命令

    Redis 2.6.12 之后,SET 命令增加了 NXPX 等参数,解决了原子性问题。

    bash 复制代码
    SET lock_key client_id NX PX 30000
    • NX:仅当键不存在时设置。
    • PX:设置毫秒级过期时间。

    遗留问题:如果业务执行时间超过30秒,锁会被自动释放,导致其他客户端获取锁,引发并发冲突。此外,如果客户端A持有锁期间宕机,锁虽然会自动释放,但无法感知锁是否被"误删"(例如A恢复后删除了B持有的锁)。

  3. 黄金时代:Redisson与看门狗

    为了解决"业务未执行完锁过期"的问题,业界引入了Redisson 。其核心在于WatchDog(看门狗)机制

    WatchDog原理

    当客户端获取锁时,如果不指定租约时间,Redisson会默认设置30秒过期时间,并启动一个后台定时任务(WatchDog)。每隔10秒,它会检查客户端是否还持有锁,如果持有,则重置过期时间为30秒。

    流程图解

二、至暗时刻:时钟跳跃与GC停顿

在生产环境中,理论上的完美锁往往不堪一击。

  1. GC停顿(Stop-The-World)

    假设客户端A获取了锁,但在执行关键业务逻辑时,JVM发生了长时间的Full GC(例如暂停了35秒)。此时,Redis中的锁(30秒过期)已经自动释放。客户端B顺利获取了锁并开始操作。当A的GC结束时,它并不知道自己已经"掉线",继续操作共享资源,导致并发冲突。
    解决方案 :虽然WatchDog能缓解一部分问题,但根本解决需要依赖Fencing Token(围栏令牌) 机制,即在资源服务端检查操作请求的顺序,拒绝旧版本的请求。

  2. 时钟跳跃(Clock Drift)

    Redis服务器和客户端的时间可能不同步。如果Redis服务器时间跳变(例如NTP校准导致时间向前跳跃),锁的剩余生存时间可能会意外缩短,导致锁提前释放。

三、终极方案之争:Redlock算法与争议

为了解决单点故障(主从切换导致锁丢失)的问题,Redis作者antirez提出了Redlock算法。

Redlock核心步骤

  1. 获取当前时间(毫秒)。
  2. 依次向N个(通常5个)独立的Redis节点发送加锁请求(设置极短的超时时间)。
  3. 计算获取锁的耗时。
  4. 如果成功加锁的节点数 > N/2耗时 < 锁有效期,则加锁成功。
  5. 如果失败,向所有节点发送释放锁请求。

流程图解

Martin Kleppmann的质疑

Martin指出,Redlock依赖系统时钟的稳定性。如果发生网络分区,或者某个节点因为GC停顿导致响应变慢,可能会破坏算法的安全性。此外,如果没有Fencing Token,Redlock也不能保证强一致性。

四、架构选型:Redis vs ZooKeeper

特性 Redis分布式锁 ZooKeeper分布式锁
一致性模型 AP(最终一致性,存在主从切换丢锁风险) CP(强一致性,基于ZAB协议)
实现复杂度 低(尤其是使用Redisson) 高(需要维护ZK集群,API较繁琐)
性能 极高(内存操作,微秒级) 较高(但在高并发写场景下不如Redis)
可靠性 中等(受GC、时钟影响) 极高(临时顺序节点天然防死锁)
适用场景 高并发、对锁可靠性要求非绝对严苛(如缓存击穿、普通秒杀) 金融级交易、配置管理、对一致性要求极高

五、常见面试题

Q1: Redis分布式锁的"误删"问题是如何产生的?如何解决?

A: 产生原因:客户端A获取锁,因GC卡顿导致业务执行时间超过锁的TTL,锁自动释放。客户端B获取锁。A恢复后执行删除操作,误删了B的锁。
解决: 必须使用Lua脚本保证"判断Value是否属于自己"和"删除"的原子性。即:if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end

Q2: Redlock算法在什么情况下会失效?

A: 1. 网络延迟导致大部分节点加锁超时;2. 节点发生时钟跳跃(向后或向前);3. 客户端发生长时间GC停顿,导致锁在持有期间过期(这是所有基于TTL锁的通病)。

Q3: 如何设计一个可重入的分布式锁?

A: 利用Hash结构。Key为锁名,Field为UUID:ThreadId,Value为重入次数。加锁时,如果Field存在且匹配,则Value+1;否则执行SETNX。解锁时Value-1,为0时删除Key。

相关推荐
ZC跨境爬虫2 小时前
Scrapy分布式爬虫(单机模拟多节点):豆瓣Top250项目设置与数据流全解析
分布式·爬虫·python·scrapy
Polar__Star2 小时前
C#怎么操作Chart图表控件 C#如何用WinForms Chart控件绑定数据绘制统计图表【控件】
jvm·数据库·python
2401_897190552 小时前
CSS如何制作数字滚动效果_利用transform位移数字
jvm·数据库·python
一 乐3 小时前
电影院|基于springboot + vue电影院购票管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·电影院购票管理管理系统
1.14(java)3 小时前
Spring核心:IoC与DI详解
数据库
运维 小白3 小时前
PostgreSQL高可用(Patroni + etcd + Keepalived)
数据库·postgresql·etcd
2301_813599553 小时前
HTML图片怎么用UnoCSS对齐_UnoCSS原子化CSS图片对齐实战
jvm·数据库·python
m0_377618233 小时前
c++怎么在不加载整个大文件的情况下获取其SHA256校验值【进阶】
jvm·数据库·python
檬柠wan3 小时前
MySQL-数据库增删改查学习
数据库·学习·mysql