从sexnx到Redission:成功的分布式锁做对了什么?锁续期/可重入/共识确保一致性。but:时钟跳跃挑战!

263 阅读7分钟

咱们今天就来聊聊Redission这个分布式锁的利器,从最简单的想法开始,一步步拆解它的演进过程,最后逼近如今最牛的方案。


从最简单的锁说起:单点Redis锁

咱们先从最基础的玩意儿入手,假设你有个分布式系统,要保证多个服务不会同时改同一个资源。最直观的办法是啥?当然是用Redis搞个锁啊!Redis速度快,又是单线程的,用它来做锁再合适不过了。咱可以用SETNX命令(set if not exists),意思是“如果这个key不存在就设置,存在就失败”。比如:

  • 服务A跑过来,执行SETNX lock_key 1,成功,锁住了。
  • 服务B也来试,SETNX lock_key 1,失败,只能等着。

锁住之后,干完活儿再DEL lock_key,释放锁,多简单!为了防止服务A挂掉锁不释放,还可以加个过期时间,比如SET lock_key 1 EX 10 NX,10秒后自动过期。

这法子听着挺美,但问题很快就冒出来了。你想想,如果服务A拿到锁后,干活儿干到一半,机器宕了呢?虽然有10秒过期,可万一活儿本身就需要15秒怎么办?锁过期了,服务B拿到锁,俩服务一块儿改数据,那不就乱套了?还有,Redis是单点部署,万一这台Redis挂了,锁机制直接歇菜。更别提主从复制的场景了,主库写完还没同步到从库,主库崩了,从库压根不知道有锁,服务B照样能抢到。

这“朴素”方案的毛病很明显:

  1. 单点故障:Redis挂了,全完。
  2. 超时风险:活儿没干完锁就没了,数据一致性没了保障。
  3. 主从不一致:异步复制下,锁可能“丢”了。

第一步优化:加个看门狗

面对超时问题,咱们得想办法让锁聪明点。Redission这时候就露头了,它引入了一个叫“看门狗”(watchdog)的机制。简单说,就是锁不是傻乎乎地等过期,而是能动态续命。

咋实现的呢?Redission在你拿到锁后,会启动一个后台任务,默认每隔10秒检查一次。如果发现你还在用锁(比如线程没挂,客户端没断),就自动把锁的过期时间延长,比如再加30秒。这样,只要服务没崩,锁就不会丢。代码大概是这样的伪逻辑:

RLock lock = redisson.getLock("myLock");
lock.lock(); // 默认30秒过期
// 看门狗每10秒检查,自动续到30秒
doSomething();
lock.unlock();

这招解决了超时问题,但单点Redis的毛病还在。如果Redis挂了,看门狗再牛也没用。而且,主从异步复制的坑也没填上——主库写了个锁还没同步,挂了,从库接管,别的服务照样能抢锁。

优化方向呢?得往“高可用”和“一致性”上靠拢。单点Redis明显不靠谱,得考虑多节点部署,主从得同步得更严实。这不就逼着咱们往分布式锁的高级玩法走了吗?


横向对比:Redission vs 普通Redis锁

先停下来横向瞅一眼,Redission跟普通的Redis锁比,强在哪儿?除了看门狗,Redission还支持可重入锁(Reentrant Lock)。啥意思呢?就是同一个线程可以多次拿锁,不会被自己挡住。比如:

RLock lock = redisson.getLock("myLock");
lock.lock();
lock.lock(); // 可重入,再锁一次没事
lock.unlock();
lock.unlock();

普通SETNX可没这本事,你拿一次锁再拿就失败,得自己额外维护个计数器,麻烦得要死。Redission底层用了个Hash结构,key是锁名,field是线程ID,value是重入次数,这样就优雅地解决了。

再看公平锁(Fair Lock),Redission还能保证先来的先拿锁,避免“饿死”问题。普通Redis锁是抢到算谁的,后来的可能老抢到,先来的傻等。Redission通过队列机制,把请求排好队,谁先来谁先上。

但这些优点还是基于单点Redis或者简单的主从部署,碰到主从不一致或者单点崩盘,还是得跪。所以,Redission真正的杀手锏还在后头——RedLock。


纵向深入:从单锁到RedLock的演进

单点Redis靠不住,主从复制又有延迟,那咋整?Redission祭出了RedLock,把多个独立的Redis节点绑一块儿,搞了个“多数派”策略。这思路跟分布式系统里的共识算法有点像,比如Paxos、Raft,核心是:只要大多数节点同意,锁就算拿到了。

具体咋干的?假设你有5个独立Redis节点(不是一个集群的5个分片,是5个完全独立的实例),RedLock的步骤是:

  1. 服务A挨个儿去这5个节点拿锁,用SETNX加过期时间,比如5秒。
  2. 只要拿到3个以上(5/2 + 1 = 3)的锁,就算成功。
  3. 如果没拿到一半以上,锁就作废,把已经拿到的释放掉。
  4. 拿到锁后,算一下整个过程花了多久(比如2秒),锁的有效时间得减去这2秒,只剩3秒。

为啥要多数派?因为单个节点不可靠,哪怕1个或2个节点挂了,只要3个还活着,锁的可靠性就有保障。比起单点Redis,这不就是从“单枪匹马”进化到“群策群力”了吗?

代码上,Redission的RedLock长这样:

RLock lock1 = redisson1.getLock("myLock");
RLock lock2 = redisson2.getLock("myLock");
RLock lock3 = redisson3.getLock("myLock");
RLock redLock = redisson.getRedLock(lock1, lock2, lock3);
redLock.lock(); // 锁住3个节点才算成功
doSomething();
redLock.unlock();

RedLock的核心优势与实现细节

RedLock最大的亮点就是把多个RLock合并成一个“超级锁”,解决了单点和主从的问题。但这背后有啥讲究呢?

  • 容错性:5个节点,挂2个没事,3个还能干活儿。这比单点Redis强太多了。
  • 一致性:不像主从复制那样异步,RedLock要求每个节点独立确认,拿到多数票才生效,降低了“假锁”的风险。
  • 性能代价:挨个儿锁5个节点,网络延迟叠加,拿到锁的时间可能从几毫秒变几十毫秒,得掂量业务能不能接受。

实现上,Redission用了个RedissonMultiLock类,RedLock继承自它。每次lock(),它会并行请求所有节点,统计成功数,超过一半就返回成功,否则回滚。释放锁的时候也一样,得通知所有节点,确保没遗漏。


再挖挖坑:RedLock也不是万能的

别以为RedLock就完美了,它也有软肋。Martin Kleppmann(分布式系统大牛)就喷过,说RedLock对时间依赖太强。咋回事儿呢?假设5个节点里,有个节点的时钟跳跃了,锁提前过期,别的服务可能误以为锁没了,抢进来。这时候一致性就崩了。

还有,如果网络抖得厉害,请求5个节点可能超时,锁拿不到,业务就卡住了。Redission虽然尽量并行优化,但网络这东西谁也管不了。

优化方向咋走?得加点“次序保证”,比如用单调递增的token(fencing token),确保后来的锁能覆盖前面的。RedLock现在缺这玩意儿,得靠额外的共识机制补,比如ZooKeeper或者etcd。这不就跟主流分布式锁方案接轨了嘛?


总结:从朴素到复杂的逼近

从最简单的SETNX,到加看门狗的Redission单锁,再到多节点合作的RedLock,咱们一步步逼近了分布式锁的“终极形态”。朴素方案的单点、超时、主从问题,被高可用、一致性的需求推着往前走。Redission的RedLock用多数派策略干掉了不少坑,但时间依赖和网络开销又冒出来,最终还得往更严谨的共识算法靠拢。

这过程就像修车,从换个轮胎到改发动机,最后发现还得整个自动驾驶系统。Redission的优势在于,它把这些复杂性封装得挺好用,尤其是RedLock的“多锁合一”,直接让开发者少操了不少心。横向看,它比普通Redis锁功能丰富;纵向看,它从单点进化到分布式,思路跟业界主流不谋而合。牛不牛?你说了算!