Redis分布式锁到底靠不靠谱
摘要:# Redis分布式锁,真能锁得住吗? 我前两天刚处理一个线上问题,凌晨三点被电话叫醒,一看监控——同一个优惠券被重复核销了十几次。团队里的小伙子信誓旦旦地说:“我用了Redis分布式锁,绝对没问题!” 结果呢?问题就出在这个“绝对没问题”上。 说实话…
Redis分布式锁,真能锁得住吗?
我前两天刚处理一个线上问题,凌晨三点被电话叫醒,一看监控——同一个优惠券被重复核销了十几次。团队里的小伙子信誓旦旦地说:“我用了Redis分布式锁,绝对没问题!” 结果呢?问题就出在这个“绝对没问题”上。
说实话,现在随便找个技术文章,说到分布式锁,十个有九个会提Redis。但很多人用Redis做分布式锁,就像拿着把玩具锁去锁银行金库——看上去是个锁,真遇上事儿了,一捅就开。
一、Redis分布式锁,到底是怎么“锁”的?
先说最基本的玩法,就是那个著名的SETNX命令(现在推荐用SET命令带参数)。原理很简单:我在Redis里设置一个键,谁先设置成功,谁就拿到了锁。其他人再来设置,发现键已经存在,就等着。
听起来挺美,对吧?但问题就出在这个“等着”上。
我见过不少团队,代码写得跟教科书一样标准:
if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then
return redis.call('pexpire', KEYS[1], ARGV[2])
else
return 0
end
看起来没毛病?我告诉你,毛病大了。
第一个坑:原子性问题
上面这段代码,先setnx再expire——两步操作。万一在setnx成功之后,expire执行之前,你的应用崩了呢?这个键就成了永不过期的“僵尸锁”,所有人都别想再拿到锁了。
(Redis 2.6.12之后可以用SET key value NX PX timeout一步搞定,但很多人还在用老写法,你说气不气人?)
二、那些教科书不会告诉你的“翻车现场”
我自己复盘过不少线上事故,发现Redis分布式锁翻车,往往不是锁本身的问题,而是用锁的人想得太简单。
场景一:你以为的“超时释放”
设置锁超时时间30秒,业务逻辑跑了35秒——锁自动释放了,另一个请求进来了,两个请求同时在处理同一份数据。恭喜你,数据错乱了。
更绝的是,第一个请求处理完,顺手把第二个请求刚创建的锁给删了。这下好了,锁机制彻底失效。
场景二:网络延迟的“魔术”
客户端A拿到锁,因为GC停顿或者网络延迟,锁超时释放了。客户端B拿到了锁,开始处理业务。这时候客户端A“醒”过来了,以为自己还持有锁,继续处理业务,最后把B的锁给删了。
这种场景在跨机房、网络不稳定的环境下,简直就是常态。
场景三:主从切换的“惊喜”
你用Redis单节点?那没事了。但稍微有点规模的系统,谁不用主从集群?
客户端在Master节点上拿到了锁,还没同步到Slave,Master挂了。Slave升级成Master——新Master上没有这个锁。另一个客户端来申请,成功拿到“同一把锁”。
两个客户端,都觉得自己拿到了唯一的锁,实际上各干各的。这酸爽,经历过的人都懂。
三、Redlock算法:是救星还是新坑?
Redis作者Antirez看大家用分布式锁用得这么痛苦,提出了Redlock算法。这算法在技术圈里争议不小,连Martin Kleppmann(就是写《数据密集型应用系统设计》那位大佬)都专门写文章怼过。
Redlock的核心思路是:你搞多个独立的Redis实例(不是主从,是真正独立的),超过半数实例设置成功才算拿到锁。
听起来很严谨对吧?但Martin指出几个致命问题:
- 时钟跳跃问题:如果某个Redis实例的时钟突然跳了,锁可能提前释放
- GC停顿问题:客户端GC停顿期间,锁可能过期,但客户端不知道
- 性能代价:每次加锁解锁都要操作多个实例,延迟上去了
说实话,Redlock在理论上确实更安全,但实现复杂,性能损耗大。很多团队根本用不对,最后变成了“高配版的错误用法”。
四、那到底该怎么用?
先说结论:Redis分布式锁,在特定场景下是靠谱的,但你别指望它万能。
如果你非要用,记住这几个原则:
1. 锁的用途要明确
- 用来防止重复提交?可以
- 用来扣减库存?慎重
- 用来处理金融交易?我劝你再想想
说白了,Redis分布式锁最适合的是那些“丢了也无所谓”的场景。比如防止用户连续点击提交按钮,这种场景下,就算锁偶尔失效,顶多让用户多提交一次,不会造成灾难性后果。
2. 超时时间要合理
别拍脑袋定个30秒。你得实际压测,知道你的业务逻辑到底要跑多久。然后在最长时间的基础上,再加个缓冲时间。
我自己的经验是:定时任务巡检锁状态。如果发现锁快过期了但业务还没跑完,就自动续期。很多客户端库(比如Redisson)已经实现了这个功能。
3. 一定要设置锁标识
每个客户端用唯一的UUID作为锁的值,删除锁的时候,先判断是不是自己的锁。别傻乎乎地直接DEL。
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
4. 准备好降级方案
锁没拿到怎么办?是直接返回错误,还是排队重试?重试几次?重试间隔怎么设置?
很多系统一上来就死等,等到超时,把整个系统拖垮。不如设个最大重试次数,超过次数就优雅降级。
五、什么时候该换方案?
如果你的业务符合以下特征,我劝你早点考虑其他方案:
- 强一致性要求:比如资金交易、订单处理
- 锁持有时间长:超过几秒钟
- 高并发争抢:大量客户端同时抢一把锁
这时候,该上ZooKeeper就上ZooKeeper,该用etcd就用etcd。虽然性能可能不如Redis,但人家在一致性上更靠谱。
不过话说回来,ZooKeeper也有ZooKeeper的坑——写这篇文章的时候,我手头还有个案例,某大厂因为ZooKeeper会话超时导致锁集体失效,损失惨重。没有银弹啊朋友们。
写在最后
用Redis分布式锁,就像开车上路。你开个家用轿车在市区代步,完全没问题。但你要开着它去越野、去飙车、去拉货,那就是自己找不痛快。
很多技术选型的问题,本质上不是“哪个更好”,而是“哪个更适合你的场景”。我见过太多团队,听到某个技术火,不管三七二十一就用上,最后掉坑里爬不出来。
所以下次有人问你“Redis分布式锁靠谱吗”,你可以这么回答:
“看你怎么用。用对了场景,它是把好锁;用错了地方,它就是装饰品——看着像那么回事,真遇到贼,一点用没有。”
行了,不废话了,该锁的还得锁,但记得先想清楚:你这锁,到底要锁什么?

