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

加了锁还会出问题?Redis分布式锁到底哪儿没用对啊

(引用来源:知乎等技术社区常见讨论及Redis官方文档精神)

说起Redis分布式锁,很多人觉得很简单,不就是用个SET命令加上NX和PX参数嘛,给key设个值,同时设置过期时间,拿到这个key就相当于拿到了锁,听起来万无一失,但实际情况是,很多人用的时候还是频频翻车,问题到底出在哪儿呢?根本不是Redis本身不行,而是我们用的时候,想得太简单了,漏掉了很多关键的细节,这就好比给你一把世界上最精密的锁,你却把钥匙随手插在门上,那能不出问题吗?

加了锁还会出问题?Redis分布式锁到底哪儿没用对啊

第一个最容易栽跟头的地方,就是锁的过期时间,这是为了应对一种极端情况:某个服务实例拿到锁之后,还没来得及释放,自己突然挂掉了,如果没有过期时间,那这把锁就永远得不到释放,其他服务谁也拿不到锁,整个系统就卡死了,我们必须给锁设置一个过期时间,但问题来了,这个时间设多久合适呢?

设短了,麻烦更大,想象一下,服务A拿到了锁,去处理一个很复杂的业务逻辑,这个逻辑本来预计10秒完成,所以你给锁设了10秒过期,但可能因为网络波动、数据库压力大,或者服务器本身有点卡,这个操作实际花了15秒,更糟的是,在第11秒的时候,Redis因为锁过期自动把它删除了,服务B看到锁没了,高高兴兴地拿到了锁,也开始处理同样的业务,结果就是,服务A和服务B同时在操作一份数据,脏数据、数据错乱这些问题全来了,也就是说,锁“提前”失效了,那你可能会说,我把过期时间设长点不就行了?设成30秒?那万一服务A真的在5秒就崩溃了,其他服务就得白白等上30秒,系统的吞吐量会大受影响,这个过期时间成了一个两难的选择,它只能是一个基于经验的估算值,没法保证100%覆盖业务执行时间。

加了锁还会出问题?Redis分布式锁到底哪儿没用对啊

第二个大坑,发生在解锁的时候,这个错误看起来非常低级,但偏偏很多人会犯,我们看看问题代码是怎么写的:服务A拿到了锁,锁的key是“my_lock”,对应的value是一个随机生成的唯一值,123abc”,服务A处理完业务后,需要释放锁,它直接执行了DEL my_lock命令,这有什么问题呢?

考虑这样一个场景:服务A因为某些原因(比如遇到了垃圾回收暂停),处理业务逻辑花了比较长的时间,导致它持有的锁过期了,服务B已经成功地获取了“my_lock”这把锁(value可能是“456def”),并且正在执行业务,就在服务B执行的时候,服务A终于“醒”过来了,它继续执行后续代码,毫不知情地执行了DEL my_lock,这一下,就把服务B辛苦创建的锁给删掉了!紧接着,一直在旁边等待的服务C一看锁没了,立刻趁虚而入,也拿到了锁,结果就是,服务B和服务C又形成了数据竞争,分布式锁形同虚设。

加了锁还会出问题?Redis分布式锁到底哪儿没用对啊

问题的根源在于,解锁的时候没有检查这把锁是不是“自己”的锁,正确的做法是,在删除锁之前,要先判断一下当前锁的value是不是自己当初设置的那个,因为每个客户端设置的value都是唯一的,所以这个操作是安全的,具体的步骤是:先用GET命令读取锁的value,如果和自己设置的一致,然后再用DEL命令删除,但这又引出了新问题:GETDEL是两个独立的操作,不是原子性的,万一在GET之后、DEL之前,锁恰好过期并被其他客户端获取,还是会误删别人的锁。

为了解决这个原子性问题,我们需要使用Lua脚本,可以把判断value和删除key的操作写在一个Lua脚本里,然后一次性发给Redis执行,因为Lua脚本在Redis中是单线程执行的,能保证这一系列操作的原子性,这才是安全解锁的正确姿势。

第三个让人头疼的问题是,上面说的这些操作,虽然在一个Redis实例上是安全的,但如果你用的是Redis主从架构,风险就又出现了,在主从模式下,写操作是先写到主节点,然后主节点再异步地复制给从节点,假设服务A刚在主节点上成功加锁,主节点还没来得及把这个锁信息同步给从节点,就突然宕机了,这时,哨兵机制会选举一个从节点升级为新的主节点,可惜的是,这个新的主节点上并没有服务A刚才加的那把锁的记录!服务B过来请求加锁,会顺利地从新的主节点上拿到锁,这样,服务A和服务B又同时认为自己持有锁,系统又乱套了。

为了解决这个问题,Redis的作者提出了一个叫RedLock的算法,这个算法的大致思路是,你不止和一个Redis实例打交道,而是同时向多个独立的Redis主节点(通常是5个)发起加锁请求,只有当超过半数的节点(比如3个)都加锁成功,才算真正拿到锁,这样,即使某个主节点宕机了,只要不超过半数,就不会出现一把锁被两个客户端同时拿到的情况,RedLock算法本身也非常复杂,争议很大,因为它依赖多个Redis实例的时钟大致同步,并且网络延迟也会带来新的不确定性,是不是要用RedLock,还得根据业务的极端严格程度来权衡。

所以你看,一个看似简单的Redis分布式锁,背后藏着这么多玄机,从设置合理的过期时间,到实现安全的原子解锁,再到应对主从切换的极端情况,每一步都需要仔细考量,直接用几个简单的命令,而不去思考这些边界条件,那分布式锁出问题就是必然的,它不是一个即插即用的工具,而是一个需要根据自身业务场景精心设计和维护的组件。