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

Redis分布式锁怎么用乐观锁搞得更安全点,分布式环境下的那些事儿

关于怎么用乐观锁让Redis分布式锁更安全这事儿,咱们得先聊聊分布式环境下用Redis做锁最容易碰上哪些头疼事儿,你不能光知道用那个SET key value NX PX命令把锁抢到手就完事儿了,后面藏着不少坑呢。(来源:基于Redis官方文档对SET命令NX、PX参数的说明以及社区常见的锁实现模式)

最经典的一个问题就是,你锁设了个30秒过期时间,结果你的业务代码跑了35秒还没完,这时候,锁因为到期自动释放了,另一个等着的工作进程一看锁没了,高高兴兴地拿到了锁开始干自己的活儿,可没过几秒,最开始那个慢吞吞的进程终于干完活了,它傻乎乎地跑去执行删除锁的命令,这一删,坏事了,它把第二个进程刚拿到手的锁给删了!锁的安全性彻底崩盘。(来源:Martin Kleppmann的《How to do distributed locking》一文中详细描述过此场景)

这其实就是锁的持有者身份不明晰导致的,你删锁的时候,根本不知道眼前这把锁是不是当初自己设置的那把,为了解决这个问题,就得引入“乐观锁”的那种思想精髓,啥是乐观锁?简单说就是,我默认大家不会老是冲突,但在动手改数据(比如删锁)的那一刹那,我得先检查一下这数据从我上次看到现在,有没有被别人动过手脚。(来源:乐观锁是数据库等领域的一个经典概念,常用于并发控制)

具体到Redis锁上,做法特别实在,就是在你设置锁的时候,别随便用一个固定的值当value,你得用一个全球唯一的、别人根本猜不着的字符串,比如UUID或者结合机器标识的序列号,这个value,就是你作为锁持有者的“身份证”。(来源:Redis分布式锁的最佳实践中普遍推荐为每个锁设置唯一随机值)

关键步骤来了:当你要释放锁的时候,不能直接DEL key,你得先用一个Redis命令,比如GET,去把当前锁的value读出来,然后跟你自己当初设置的那个value比一比,如果一模一样,说明这把锁从始至终都是你占着的,那你就可以放心地DEL掉它,如果不一样,对不起,这锁肯定已经换过主人了,你千万别动它,老老实实放弃删除操作。(来源:Redis官方文档在Distributed Locks章节中建议使用Lua脚本来实现校验值与删除的原子性操作)

注意了,上面这个“先GET比对,再DEL删除”的操作,在分布式环境下本身又是个非原子性的操作!你刚GET完,确认value是对的,正准备DEL呢,可能锁恰好过期,又被另一个进程抢去设置了新value,这时候你的DEL还是会误删,光有乐观锁的思想还不够,必须保证“检查身份”和“删除锁”这两个动作是连续、不可分割的,也就是原子性的。(来源:同样源于对并发竞态条件的分析,以及Redis Lua脚本解决此类问题的必要性)

那在Redis里,怎么保证原子性?答案就是用Lua脚本,你可以写一小段Lua代码,把它发送到Redis服务器上去执行,服务器保证这段脚本在执行过程中不会被其他命令打断,脚本的内容就是刚才我们说的逻辑:

if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

(来源:这段Lua脚本是Redis社区为实现安全释放锁而总结出的标准写法) 这把锁的键名是KEYS[1],你持有的唯一value是ARGV[1],Redis服务器会一口气执行完判断和删除,这样一来,上面提到的那个在GET之后、DEL之前锁被他人抢占的问题就彻底解决了,这才是用乐观锁思想加固Redis分布式锁安全性的核心所在。

除了这个最要命的问题,还有一些其他事儿也得放在心上,它们同样影响着锁的安全性,你得确保Redis本身是高可用的,如果用的单机Redis,它宕机了,那所有的锁都失效了,所以生产环境一般用Redis哨兵或者集群模式,但这又可能引出新的问题,比如在发生主从切换的极短暂瞬间,可能会因为数据同步延迟导致锁信息丢失,出现两个客户端同时持有同一把锁的情况,对于这种极端场景,Redis的作者提出了一种叫RedLock的算法,试图通过同时向多个独立的Redis实例申请锁来达成更高的一致性,但这个算法本身在学术界和工程界也有不少争论,认为它依赖了不现实的假设(比如所有节点时钟同步),是否要用RedLock,得看你对锁的安全级别要求到底有多高,以及是否能接受它带来的复杂性和性能开销。(来源:RedLock算法由Redis作者Salvatore Sanfilippo提出,其可靠性争议可参考Martin Kleppmann的批评文章以及Antirez的反驳)

再比如,锁的过期时间设置多少合适?设短了,容易发生业务没完锁先超时的悲剧;设长了,万一持有锁的客户端真宕机了,其他进程又得傻等很久系统才能恢复,一个常见的优化办法是,起一个单独的守护线程,在业务处理期间,定期去检查锁是否还在持有,并且如果业务还在进行中,就不断地稍微延长一点锁的过期时间(俗称“看门狗”机制),但这同样增加了实现的复杂度。(来源:一些成熟的Redis客户端,如Redisson,内置了这种watchdog机制来续期锁)

所以总结一下,用Redis搞分布式锁,想靠乐观锁的思路搞得更安全,核心就是两件事:第一,给锁加上全局唯一的“持有人标识”(value);第二,必须通过原子操作(比如Lua脚本)来验证身份后再释放锁,这是守住安全底线的关键,在此基础上,再根据你的业务容忍度,去考虑要不要处理主从切换、怎么设置超时时间、要不要自动续期这些更复杂的问题,在分布式环境下,没有一劳永逸的银弹,每一步都得琢磨清楚背后的风险和代价。

Redis分布式锁怎么用乐观锁搞得更安全点,分布式环境下的那些事儿