🔒️ 面试官:你会几种Redis分布式锁?我会三种!

阿轩的BUG
文章191
评论56
标签101
站点日志
作者总数:1位
置顶文章:0篇
标签总数:101条
文章总数:196篇
评论总数:56条
微语总数:201条
运行天数:2317天
最近更新:2026-07-28
文章标签
最新评论
Sherlock
2025-08-21
不是应该打包成apk文件吗,怎么突然就运行编译了,下一步呢
老金er
2025-05-25
人生设计非常必要,在信息化发达的今天,周围大部分人依然只能过着人生工程模式生活,长时间生活洗礼后,尽管“人生工程”已经不再适合很多人,他们也无法做出任何改变,人生设计给我们提供了新的思路,值得尝试
赏帮赚
2025-05-25
职场太多的尔虞我诈
三笑
2024-08-16
emlog6.0.1可以升级支持吗?
pony
2024-07-08
这篇文章提供了一种新的人生规划方法——"人生设计",它借鉴了产品设计的理念,鼓励人们以创造性和探索性的方式思考和规划自己的生活。以下是对这篇文章的几个评价维度: 创新性:将产品设计的理念应用于人生规划是一个新颖的视角,为人们提供了一种跳出传统生活轨迹,探索个性化生活路径的方法。 实用性:文章不仅提出了理念,还详细介绍了具体的实施步骤,包括自我评估、制定计划、原型设计和实践选择等,具有很强的操作性。 启发性:通过强调好奇心、不断尝试、重新定义问题等心态,文章鼓励人们打破常规,勇于探索未知,这对于激发个人潜能和创造力具有积极作用。 适应性:"人生设计"理念适用于不同人生阶段和不同背景的人,无论是希望从头开始的人,还是已经取得一定成就但希望探索新方向的人,都可以从中获得启发。 情感共鸣:文章对许多人对传统生活轨迹的不满和对改变的渴望进行了深刻的洞察,能够引起读者的共鸣。 可持续性:人生设计强调的是一个持续迭代和试错的过程,这与现实生活的发展规律相吻合,有助于人们在不断变化的环境中做出适应性调整。 教育意义:作为一种人生规划方法,"人生设计"可以作为教育的一部分,帮助年轻人更早地学会自我探索和规划,为未来的生活和职业发展打下基础。 局限性:尽管"人生设计"提供了一种新的思考框架,但它可能需要个人具备一定的自我认知和反思能力,对于一些缺乏自我探索经验的人来说,可能需要额外的指导和支持。 总体来说,这篇文章提供了一种有价值的人生规划方法,对于那些渴望改变、寻求个性化生活的人来说,是一种有益的参考和指导。
首页实用干货 🌋

大家春节在家抢红包玩的不亦乐乎,抢红包服务看起来非常简单,实际上要做好这个服务,特别是money相关服务是不允许出错的,想想看每个红包的数字都是真金白银,要求服务的鲁棒性非常高,背后包含着很多后台服务技术细节。
今天就来说说高并发服务编程中的redis分布式锁。
这里罗列出3种redis实现的分布式锁,并分别对比说明各自特点。

Redis单实例分布式锁


实现一: SETNX实现的分布式锁

setnx用法参考redis官方文档

语法
SETNX key value

将key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做。SETNX是”SET if Not eXists”的简写。
返回值:

  • 1 设置key成功
  • 0 设置key失败
加锁步骤
SETNX lock.foo <current Unix time + lock timeout + 1>

如果客户端获得锁,SETNX返回1,加锁成功。
如果SETNX返回0,那么该键已经被其他的客户端锁定。

1、接上一步,SETNX返回0加锁失败,此时,调用GET lock.foo获取时间戳检查该锁是否已经过期:
2、如果没有过期,则休眠一会重试。

3、如果已经过期,则可以获取该锁。具体的:调用 GETSET lock.foo <current Unix timestamp + lock timeout + 1> 基于当前时间设置新的过期时间。
注意: 这里设置的时候因为在 SETNXGETSET 之间有个窗口期,在这期间锁可能已被其他客户端抢去,所以这里需要判断 GETSET 的返回值,他的返回值是 SET 之前旧的时间戳:

  • 若旧的时间戳已过期,则表示加锁成功。
  • 若旧的时间戳还未过期(说明被其他客户端抢去并设置了时间戳),代表加锁失败,需要等待重试。
解锁步骤

解锁相对简单,只需 GET lock.foo 时间戳,判断是否过期,过期就调用删除 DEL lock.foo

实现二:SET实现的分布式锁

set用法参考官方文档

语法
SET key value [EX seconds|PX milliseconds] [NX|XX]

将键 key 设定为指定的 “字符串” 值。如果 key 已经保存了一个值,那么这个操作会直接覆盖原来的值,并且忽略原始类型。当 set 命令执行成功之后,之前设置的过期时间都将失效。
从2.6.12版本开始,redis为 SET 命令增加了一系列选项:

  • EXseconds – Set the specified expire time, in seconds.
  • PXmilliseconds – Set the specified expire time, in milliseconds.
  • NX – Only set the key if it does not already exist.
  • XX – Only set the key if it already exist.
  • EXseconds – 设置键key的过期时间,单位时秒
  • PXmilliseconds – 设置键key的过期时间,单位是毫秒
  • NX – 只有键key不存在的时候才会设置key的值
  • XX – 只有键key存在的时候才会设置key的值

版本>= 6.0

KEEPTTL — 保持 key 之前的有效时间TTL

加锁步骤

一条命令即可加锁: SET resource_name my_random_value NX PX 30000

The command will set the key only if it does not already exist (NX option), with an expire of 30000 milliseconds (PX option). The key is set to a value “myrandomvalue”. This value must be unique across all clients and all lock requests.

这个命令只有当 key 对应的键不存在resource_name时(NX选项的作用)才生效,同时设置30000毫秒的超时,成功设置其值为my_random_value,这是个在所有redis客户端加锁请求中全局唯一的随机值。

解锁步骤

解锁时需要确保my_random_value和加锁的时候一致。下面的Lua脚本可以完成

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

这段Lua脚本在执行的时候要把前面的 my_random_value 作为 ARGV[1] 的值传进去,把 resource_name 作为 KEYS[1] 的值传进去。释放锁其实包含三步操作:’GET’、判断和’DEL’,用Lua脚本来实现能保证这三步的原子性。

Redis集群分布式锁

实现三:Redlock

前面两种分布式锁的实现都是针对单redis master实例,既不是有互为备份的slave节点也不是多master集群,如果是redis集群,每个redis master节点都是独立存储,这种场景用前面两种加锁策略有锁的安全性问题。
比如下面这种场景:

1、客户端1从Master获取了锁。
2、Master宕机了,存储锁的key还没有来得及同步到Slave上。
3、Slave升级为Master。
4、客户端2从新的Master获取到了对应同一个资源的锁。

于是,客户端1和客户端2同时持有了同一个资源的锁。锁的安全性被打破。
针对这种多redis服务实例的场景,redis作者antirez设计了Redlock (Distributed locks with Redis)算法,就是我们接下来介绍的。

加锁步骤

集群加锁的总体思想是尝试锁住所有节点,当有一半以上节点被锁住就代表加锁成功。集群部署你的数据可能保存在任何一个redis服务节点上,一旦加锁必须确保集群内任意节点被锁住,否则也就失去了加锁的意义。

具体的:

1、获取当前时间(毫秒数)。
2、按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过期时间(比如PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。
3、计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。
4、如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。
5、如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的Redis Lua脚本)。

解锁步骤

客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否。

算法实现

上面描述的算法已经有现成的实现,各种语言版本。

总结


综上所述,三种实现方式。

  • 单redis实例场景,分布式锁实现一和实现二都可以,实现二更简洁推荐用实现二,用实现三也可以,但是实现三有点复杂略显笨重。
  • 多redis实例场景推荐用实现三最安全,不过实现三也不是完美无瑕,也有针对这种算法缺陷的讨论(节点宕机同步时延、时间同步假设),大家还需要根据自身业务场景灵活选择或定制自己的分布式锁。
上一篇
💡 JavaScript 中 (0, function)(param) 是什么?
下一篇
🏷️ 为什么我避免使用async/await?