Redis锁到底怎么保证同步,背后机制其实挺有意思的
- 问答
- 2025-12-31 05:31:04
- 2
主要整合自多位资深开发者的技术博客分享、Redis官方文档关于分布式锁的说明,以及马丁·克莱普曼(Martin Kleppmann)关于分布式锁的经典论文分析)
Redis锁,我们通常指的是用它来实现一种“分布式锁”,想象一下,在一个系统里,有很多台机器上的很多个程序(或线程),它们都想同时去修改同一个东西,比如抢着去清空一个商品的库存,如果让它们一窝蜂地上,库存肯定就乱套了,可能一件商品被卖出去好几十次,这时候,就需要一把“锁”来保证同一时间只有一个程序能进去操作,Redis锁就是干这个用的。
那它最基本的思想是什么呢?
特别简单,占坑”原理,Redis就像一个公共的布告栏,一个程序(我们叫它客户端A)想要操作库存之前,先跑到Redis那里,在指定的键(key)上设置一个唯一的值,这个键的名字可以叫“lock:stock_123”,这个唯一的值可以是UUID或者别的能绝对区分不同客户端的东西。
设置这个键值的时候,有个关键操作:必须带上NX(Not eXists)参数,这个NX参数的意思是:“只有当这个‘lock:stock_123’坑位不存在的时候,我才去设置它;如果已经有人占了,那我就设置失败。” 这就好比第一个到厕所的人把门锁上,后来的人看到门锁了,就知道里面有人,得等着。
光占坑还不行,还得有个“租期”,因为万一占坑的程序(客户端A)因为某种原因(比如机器宕机了)卡住了,或者处理业务逻辑时间太长,一直不释放这个锁,那其他所有程序不就永远傻等着了吗?整个系统就卡死了,在设置锁的时候,还必须同时设置一个过期时间(PX参数),比如5秒,这样,即使客户端A挂了,5秒钟后Redis也会自动把这个锁删除,让其他程序有机会获得锁,这就像给厕所门锁加了个自动解锁功能,防止有人晕倒在里面外面的人进不去。
一个完整的加锁命令看起来是这样的:SET lock:stock_123 my_unique_value NX PX 5000,意思是:设置键lock:stock_123的值为my_unique_value,仅当该键不存在时设置成功,并且给这个键设置5秒的过期时间。
看起来完美了?其实坑才刚刚开始。 上面这个简单的模型有几个致命的问题,Redis锁的“有意思”之处,就在于如何一步步解决这些问题。
第一个大坑:误删别人的锁。
假设客户端A拿到了锁,设置的超时时间是5秒,但是A的业务逻辑处理了6秒(可能因为GC停顿或者网络延迟),在第6秒的时候,Redis因为超时自动把A的锁给删了,这时,客户端B趁机拿到了锁,紧接着,刚处理完业务的客户端A“醒”了过来,它还以为是自己的锁呢,就顺手执行了一个DEL命令,把lock:stock_123给删了——结果删掉的是客户端B刚创建的锁!这下客户端C也能拿到锁了,同步就失效了。
怎么解决?答案是要“验明正身”。
在删除锁之前,要先确认这个锁是不是自己当初设置的那个,这就用到了设置锁时那个“唯一的值”(my_unique_value),删除的正确姿势不是直接用DEL,而是用一段Lua脚本(来源:Redis官方推荐做法),Lua脚本能保证多个命令的原子性执行。
脚本逻辑大概是这样的:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
解释一下:KEYS[1]就是锁的key(lock:stock_123),ARGV[1]就是客户端A自己设置的那个唯一值(my_unique_value),脚本会先检查当前锁的值是不是自己的那个值,如果是,才删除;如果不是,说明锁已经属于别人了,就返回0表示删除失败,这样就避免了误删。
第二个更深的坑:锁过期时间不好评估。 这就是上面提到的“客户端A业务没处理完,锁却过期了”的问题,这个问题更难解决,因为业务逻辑的执行时间是不确定的,一个常见的进阶方案叫“锁续期”(来源:市面上成熟的Redis客户端如Redisson的实现机制)。
简单说,就是客户端A在拿到锁之后,同时启动一个额外的“看门狗”线程(或者定时任务),这个看门狗会每隔一段时间(比如锁过期时间的1/3,即5秒的锁就每隔1.6秒左右),去检查一下业务是否还在执行,并且锁是否还是被自己持有,如果是,它就通过Redis命令延长这个锁的过期时间(比如重新设置为5秒后过期),这样,只要客户端A还“活着”并且在正常处理业务,这个锁就不会因为超时而被自动释放,相当于实现了锁的自动延期,只有当客户端A真正完成业务,主动释放锁,或者客户端A整个进程挂掉了,看门狗线程也随之停止,锁才会在最终的过期时间后释放。
第三个终极难题:Redis本身的高可用。 如果我们的Redis是单机的,万一这台Redis服务器宕机了,那所有的锁信息就都丢失了,即使有客户端还认为自己持有锁,但实际上锁已经不存在了,其他客户端就可以在新的Redis主节点上重新加锁, again,同步被破坏。
为了解决这个问题,人们提出了Redlock算法(来源:Redis作者Antirez提出的分布式锁算法),这个算法相对复杂,其核心思想是:客户端不再只和一个Redis实例打交道,而是和多个(通常是5个)相互独立的Redis主节点(注意不是主从关系)打交道。
加锁时,客户端向这5个实例依次发送加锁请求(带唯一值和过期时间),只有当超过半数的实例(比如3个或以上)都加锁成功,并且总耗时小于锁的过期时间,才算加锁成功,这样,即使有个别Redis节点宕机,只要大多数节点还活着,锁服务就是可用的,同样,释放锁时也需要向所有节点发起释放请求。
Redlock算法也引发了很大的争议(来源:马丁·克莱普曼的论文指出了其潜在问题),比如它严重依赖系统时钟的准确性,在某些极端故障场景下依然可能失效,是否使用Redlock,需要根据业务对数据一致性的要求级别来权衡。
Redis锁保证同步的机制,是一个从简到繁、不断修补漏洞的过程:从最基本的“NX+PX占坑”,到用Lua脚本避免误删,再到用看门狗续期解决超时,最后为了高可用考虑多节点部署的Redlock,每一步都是为了解决在分布式环境下遇到的新挑战,它背后的机制之所以有意思,正是因为它生动地体现了在分布式系统中,没有任何银弹,安全和效率总是在不断博弈和权衡。

本文由凤伟才于2025-12-31发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:http://waw.haoid.cn/wenda/71701.html
