当前位置:首页 > 问答 > 正文

分布式锁这东西真复杂,反复琢磨后我总结了几个靠谱方案分享给你

笔者结合过往项目经验与技术社区常见讨论总结)

分布式锁这东西,真不是一般的复杂,刚开始接触的时候,觉得不就是一把锁嘛,单机程序里用得好好的,到了分布式系统里,无非是找个大家都能访问的地方存一下锁的状态,可真动手实现或者选型的时候,各种问题就冒出来了:锁怎么算加上了?客户端挂了锁怎么释放?网络延迟了会不会导致两个客户端同时拿到锁?锁的过期时间设多久合适?这些问题就像打地鼠一样,解决一个,另一个又冒出来。

经过反复折腾和琢磨,我算是明白了,没有一种分布式锁方案是完美的,银弹不存在,不同的方案其实是在一致性、可用性、性能、复杂度之间做权衡,下面我就分享几个我觉得在大多数场景下比较靠谱的方案,以及它们各自的“坑”在哪里,帮你少走点弯路。

第一个方案,也是现在最流行的:基于Redis的分布式锁。

这大概是大家最先想到和用到的方案,思路很直观:Redis速度快,单命令是原子的,正好可以用来存一个锁的键值对,一个业务锁的key是"order_lock_123",value用一个唯一的客户端标识(比如UUID),然后执行SET key value NX PX 30000,这个命令的意思是,只有当这个key不存在(NX)时才能设置成功,并且给这个key设置30秒的过期时间(PX),如果设置成功,就算加锁成功;否则就加锁失败。

这套方案的核心要点有几个:

  1. 必须设置过期时间:这是为了防止客户端加锁后崩溃,导致锁永远无法释放,变成“死锁”,过期时间是个权衡,设短了可能业务没执行完锁就丢了,设长了其他客户端要等很久。
  2. value必须全局唯一:这个值用来标识加锁的客户端,释放锁的时候,要用Lua脚本先判断当前锁的value是不是自己设置的,然后再删除,这是因为要避免误删别人的锁,想象一下:客户端A加锁成功,但执行时间超过了锁的过期时间,锁自动释放了,此时客户端B加锁成功,然后客户端A执行完了,直接去释放锁,就把B的锁给删了,用Lua脚本保证“判断+删除”的原子性就能解决这个问题。

Redis锁的优点很明显:性能极高,实现起来相对简单,资料多,但它有个硬伤:它通常是基于主从异步复制的,如果主节点加锁成功后,在数据同步到从节点之前,主节点宕机了,从节点被选为新主,但这个锁的状态丢失了,其他客户端就可能再次加锁成功,导致问题,虽然Redis官方后来提出了Redlock算法来尝试解决这个问题,但Redlock本身也引发了很大的争议(比如Redis之父Antirez和分布式系统专家Martin Kleppmann之间就有过著名的辩论),它更复杂,而且并不能保证绝对的安全,对于绝大多数要求不是极端严格的场景,单Redis节点+正确设置过期时间和唯一value的方案就够用了;如果要求高,可能需要考虑下面更重的方案。

第二个方案,基于ZooKeeper的分布式锁。

Zookeeper是专门为分布式协调而设计的,用它实现分布式锁更“原生”、更严谨,常见的做法是利用它的“临时顺序节点”。

流程大概是这样的:

  1. 每个客户端都在一个指定的锁节点(比如/locks/my_lock)下,创建自己的临时顺序节点。
  2. 客户端获取/locks/my_lock下所有的子节点。
  3. 判断自己创建的子节点是不是序号最小的那个,如果是,说明自己拿到了锁。
  4. 如果不是,就对自己序号的前一个节点设置监听(watch),当前一个节点被删除时(意味着前一个客户端释放了锁),ZooKeeper会通知当前客户端,它就可以再次尝试获取锁。

这个方案的优势非常强:

  • 锁是安全的:因为ZK保证了强一致性,锁的状态在集群内是可靠的,不会出现Redis那种因主从切换导致锁丢失的问题。
  • 自动释放:临时节点的特性是,当客户端与ZK服务器的会话失效时(比如客户端宕机),节点会自动被删除,这就天然避免了死锁,不需要设置过期时间。
  • 公平锁:通过顺序节点和监听机制,客户端获取锁的顺序就是它们请求的顺序,实现了公平排队,不会出现“饿死”现象。

但缺点也同样明显:性能比Redis差很多,因为每次加锁释放锁都要创建、删除节点,还可能涉及网络通信和监听通知,你需要额外维护一个ZK集群,增加了系统复杂度,ZK锁更适合对锁的可靠性要求极高,但并发量不是天量,且已经存在ZK集群的场景。

第三个方案,基于数据库(如MySQL)的分布式锁。

这算是一个比较“朴素”和“保守”的方案,在一张表里建一个唯一索引的字段作为锁标识,加锁就是执行一条插入语句(INSERT INTO lock_table (lock_name) VALUES ('order_lock')),利用数据库的唯一约束,插入成功就算加锁成功,插入失败(因为唯一冲突)就算加锁失败,释放锁就是删除这条记录。

这个方案的优点是:理解起来最简单,如果系统里已经有数据库,几乎不需要引入新的中间件,但缺点一大堆:数据库性能是瓶颈,频繁加锁释放锁对数据库压力大;而且没有自动过期机制,如果客户端崩溃,需要手动清理,或者得靠定时任务扫描,非常麻烦,还有一种基于数据库乐观锁(用版本号)的方式,但那通常用于解决更新冲突,不太像传统的互斥锁,数据库锁一般只在非常简单的场景,或者并发量极低,并且没有其他选择时才考虑。

怎么选?看你的实际需求。

  • 要性能,能接受极小概率的锁失效:用Redis锁,并做好异常处理(比如锁失效后要有补偿机制)。
  • 要绝对可靠,宁可牺牲一些性能:用ZooKeeper锁
  • 系统简单,没啥并发,图省事:或许可以用数据库锁,但真心不推荐作为首选。

最后还得啰嗦一句,分布式锁是解决并发问题的有力工具,但也是个“重器”,在设计系统时,可以多想想是否真的需要一把“全局锁”?能不能用队列串行化处理?或者通过设计将资源分区,避免竞争?很多时候,这些方法可能比直接上分布式锁更优雅、更高效,希望我琢磨的这点经验对你有帮助。

分布式锁这东西真复杂,反复琢磨后我总结了几个靠谱方案分享给你