分布式环境下用Redis实现锁,保证操作原子性和一致性的那些事儿
- 问答
- 2026-01-03 09:07:05
- 2
主要综合了Redis官方文档关于分布式锁的说明、Martin Kleppmann的论文《How to do distributed locking》以及相关技术社区如Stack Overflow上的常见讨论)
在分布式系统里,多个服务实例经常需要同时去操作同一个资源,比如减库存、改订单状态,这时候,如果不加控制,就会乱套,可能一个库存商品被卖出去一百次,这就好比一个公共厕所只有一个坑位,大家进去之前都得先锁上门,用完了再打开,在单台机器上,我们可以用编程语言自带的锁机制来实现这个“门栓”,但在分布在不同机器上的服务之间,这个“门栓”就得找一个大家都能访问到的地方来存放——Redis,因为它速度快、简单,就成了一个很受欢迎的选择。
但用Redis实现这个分布式“门栓”(也就是锁),听起来简单,做起来却有很多细节要注意,核心就是要保证操作的原子性和一致性,原子性意思是,加锁或解锁的一系列操作必须像一个不可分割的整体一样,要么全部成功,要么全部失败,不能执行到一半被别的操作打断,一致性则要求在任何情况下,都不能出现两个客户端同时认为自己持有锁的情况。
最经典的用法,就是用一个Redis的键(Key)来代表一把锁,比如锁的名字叫“order_lock_123”,一个客户端要来操作订单123,它就先尝试在Redis里创建这个键,通常用SET命令,但这里有个关键点,不能简单地用SET key value,因为如果这个键已经存在(说明锁被别人拿走了),你直接SET会覆盖掉,这就乱套了,所以Redis官方推荐了一种更安全的方法,就是用SET key random_value NX PX 30000,这里NX参数意思是“只有当这个键不存在时才设置”,这就保证了只有第一个来设置的客户端能成功,起到了抢锁的作用,PX 30000是给这个键设置一个30秒的过期时间,为什么要设置过期时间?这是为了防止死锁,比如一个客户端抢到锁之后,还没来得及释放,就因为网络问题或者自己崩溃了,导致锁永远不被释放,其他客户端就永远拿不到锁,整个系统就卡死了,设置一个过期时间,就算客户端出事,锁也会自动过期删除,其他客户端还有机会。
光能抢锁还不行,还得能安全地释放锁,释放锁就是把这个键删掉,但删除的时候也要小心,你不能说“我抢到锁的客户端,直接去删这个键就行了”,想象一下,客户端A抢到锁,但是它的业务逻辑执行得比较慢,超过了30秒,锁自动过期了,这时候客户端B趁机抢到了锁,然后客户端A终于执行完了,它去执行删除键的操作,结果把客户端B刚创建的锁给删掉了!这就严重违反了一致性,为了解决这个问题,我们在创建锁的时候,value不能是一个固定的值,1”,而应该是一个全局唯一的值,比如一个随机的长字符串或者UUID,客户端在删除锁的时候,要先获取一下当前锁的值,跟自己当初设置的值做对比,只有一样的时候才允许删除,这个“获取值对比再删除”的操作也必须是原子的,不能分步执行,否则可能在获取值之后,锁又被别人抢走并修改了值,Redis提供了Lua脚本来保证多个命令的原子性执行,释放锁的正确姿势是,通过一个Lua脚本,先比较值,再决定是否删除。
即使做到了上述两点,还有一个著名的坑叫做“时钟漂移”问题,Martin Kleppmann在他的论文里详细讨论过这个,假设持有锁的客户端在过期时间的判断上和其他客户端或者Redis服务器本身有较大的时钟差异,客户端以为自己才用了20秒,但Redis服务器的时间走得快,认为锁已经30秒过期了,就把锁释放了,导致另一个客户端拿到锁, again 出现了两个客户端同时持有锁的风险,虽然这种情况不那么常见,但在要求极高一致性的场景下是需要考虑的,一种缓解方法是,确保所有服务器和客户端的时钟尽可能同步(比如使用NTP协议),但无法完全根除风险,对于极端重要的数据,有时需要结合数据库的悲观锁或更复杂的共识算法(如ZooKeeper)来提供更强的一致性保证。
如果业务逻辑的执行时间可能很长,超过了锁的过期时间怎么办?这被称为“锁延期”问题,一个常见的做法是,在客户端持有锁期间,启动一个后台线程或定时任务,在锁即将过期但业务还没做完时,不断地延长锁的过期时间(俗称“看门狗”机制),但这又增加了实现的复杂性。
还有一种情况是,Redis本身如果是单实例,它挂了怎么办?即使我们使用了Redis的持久化机制,在故障重启后数据能恢复,但在故障期间,锁服务是完全不可用的,为了高可用,人们会使用Redis集群(如Redis Sentinel或Redis Cluster),但在主从切换的瞬间,也可能出现一致性问题:客户端在主节点上成功创建了锁,但这个写操作还没同步到从节点,主节点就故障了,从节点升级为新主,它上面没有这个锁,另一个客户端又能成功获取锁,Redlock算法试图解决这个问题,它要求客户端在超过半数的Redis节点上都成功设置锁才算真正拿到锁,但这算法本身也引起了很大的争议,Martin Kleppmann就指出它在网络延迟、进程暂停等极端场景下仍可能失效,而且实现复杂,是否使用Redlock需要根据业务对一致性的容忍度来权衡。
用Redis实现分布式锁,一个相对安全可靠的基础方案是:使用带NX和PX/TX选项的SET命令来获取锁,值使用唯一随机数,使用比较值和删除的Lua脚本来释放锁,并设置一个合理的过期时间,但要清醒地认识到,在分布式环境下,由于网络、时钟、节点故障等各种不确定性的存在,没有任何一种锁方案能保证100%的绝对安全,它通常适用于那些需要高性能、并且允许在极罕见情况下出现一些不一致(之后可以通过其他手段修复)的场景,如果业务要求强一致性,那么可能需要寻求其他更重量级的协调服务。

本文由盈壮于2026-01-03发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://haoid.cn/wenda/73611.html
