文章目录- 前言
- 主从切换
- Redlock 红锁
- 分布式系统专家马丁的质疑
- Redis 作者 Antirez 的反驳
- 我的思考
前言
“分布式锁”这个话题在程序界有很大的关注量,引发了不少讨论。关于分布式锁有很多实现的方案,本文就讲基于 Redis 实现的分布式锁。一些基本的东西我就直接带过吧。
问:为什么需要分布式锁? 答:以前为什么需要互斥锁?
Redis 分布式锁的演进:
代码语言:javascript复制1、setnx
存在问题:若上锁的实例还没解锁就挂了,就死锁了。
解决方案:为锁设置一个过期时间。
代码语言:javascript复制2、setnx px,为锁设置一个过期时间。
存在问题:锁过期了,但是任务还没完成,锁就被释放了。当上锁者完成任务后,容易释放别的业务上的锁。
解决方案:可以采用看门狗进程,当锁要过期了就给它延时;也可以为锁设置一个token,只有token匹配上了才能解锁。
代码语言:javascript复制3、setnx key token px time
这里不采用看门狗进程的解法,具体可自己思考。
存在问题:若在解锁做 token 判断成功之后,解锁线程出现了调度,或延时,当真正操作 del 的时候锁已经过期了,则将把别的业务上的锁给解锁掉。
解决方案:将解锁过程写入 lua 脚本,将解锁过程原子化。调用 lua 脚本解锁。
lua 脚本如下:
代码语言:javascript复制--判断锁是自己的才释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
到这里,一个单节点的 redis 锁便完成了它的使命。
但是,分布式到这里还没铺开呢!!!
主从切换
我们在使用 Redis 时,一般会采用主从集群 哨兵的模式部署。当主从发生切换的时候,这个分布式锁依旧安全么?
1、客户端1在主库上上锁。 2、主库宕机,setnx 命令还未到达从库。 3、从库被哨兵提升为新主库。 4、该锁在新主库上查不到,则可以由另一个客户端上锁。
于是又开始俄罗斯套娃了。
为此,Redis 的作者提出一种解决方案,就是我们经常听到的 Redlock(红锁)。
Redlock 红锁
Redlock 的方案基于 2 个前提: 1、不再需要部署从库和哨兵实例,只部署主库。 2、但主库要部署多个,官方推荐至少 5 个实例。
问:不做主从,那万一某个库宕机了,数据不就都丢了吗? 答:丢就丢了呗,本来就不是用来存数据的。
具体流程如下:
代码语言:javascript复制1、客户端先获取当前时间戳T1。
2、客户端依次向这 5 个 Redis 实例发起加锁请求,且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),
如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁。
3、如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败。
4、加锁成功,去操作共享资源。
5、加锁失败,向「全部节点」发起释放锁请求。
问:我也不贪心,我 5 个实例来共同竞争,需要竞争多少遍(失败一遍就要全部释放)? 答:emmm…emmm…emmm…(容我三思)(思不出来)
问:为什么要在多个节点上上锁? 答:防止某些个节点因为各种原因不能提供服务,且没来得及恢复。
问:为什么释放锁,要操作所有节点? 答:无则加勉,有则改之。
二三那俩问题是网上提的比较多的,问题一是我自己提的,结果自己答不上来。。。
分布式系统专家马丁的质疑
马丁写了一篇文章(How to do distributed locking),表达了自己对于红锁的质疑。在他的文章中主要阐述了四个观点: 1、分布式锁的目的是什么? 2、锁在分布式系统中会遇到的问题。 3、假设时钟正确是不合理的。 4、提出 fencing token 的方案,保证正确性。
我们一一来看:
1、分布式锁的目的。 他认为有两个目的:
1、效率。 2、正确性。
如果是为了效率,那么单机版的 Redis 就可以了,即使偶尔锁失效,也是可以理解的。 如果是为了正确性,马丁认为 Redlock 根本达不到分布式安全的要求,依旧存在锁失效的问题。
(其实我没想明白上锁提升效率,是什么情况。)
2、锁在分布式系统中会遇到的问题 Martin 表示,一个分布式系统,更像一个复杂的「野兽」,存在着你想不到的各种异常情况。
这些异常场景主要包括三大块,这也是分布式系统会遇到的三座大山:NPC。
代码语言:javascript复制N:Network Delay,网络延迟
P:Process Pause,进程暂停(GC)
C:Clock Drift,时钟漂移
Martin 用一个进程暂停(GC)的例子,指出了 Redlock 安全性问题:
代码语言:javascript复制1、客户端 1 请求锁定节点 A、B、C、D、E
2、客户端 1 的拿到锁后,进入 GC(时间比较久)
3、所有 Redis 节点上的锁都过期了
4、客户端 2 获取到了 A、B、C、D、E 上的锁
5、客户端 1 GC 结束,认为成功获取锁
6、客户端 2 也认为获取到了锁,发生「冲突」
Martin 认为,GC 可能发生在程序的任意时刻,而且执行时间是不可控的。
当然,即使是使用没有 GC 的编程语言,在发生网络延迟、时钟漂移时,也都有可能导致 Redlock 出现问题,这里 Martin 只是拿 GC 举例。
3、假设时钟正确是不合理的。
代码语言:javascript复制客户端C1获得了对节点A、B、c的锁定,由于网络问题,法到达节点D和节点E
节点C上的时钟向前跳,导致锁提前过期
客户端C2在节点C、D、E上获得锁定,由于网络问题,无法到达A和B
客户端C1和客户端C2现在都认为他们自己持有锁
前跳应该是不至于了,不过系统时钟是存在误差的,可以看我前面发的那篇: 计算机时钟是如何运行的?
4、提出 fencing token 的方案,保证正确性
代码语言:javascript复制1、客户端在获取锁时,锁服务可以提供一个「递增」的 token
2、客户端拿着这个 token 去操作共享资源
3、共享资源可以根据 token 拒绝「后来者」的请求
这样一来,无论 NPC 哪种异常情况发生,都可以保证分布式锁的安全性,因为它是建立在「异步模型」上的。
他还表示,一个好的分布式锁,无论 NPC 怎么发生,可以不在规定时间内给出结果,但并不会给出一个错误的结果。也就是只会影响到锁的「性能」(或称之为活性),而不会影响它的「正确性」。
Redis 作者 Antirez 的反驳
在 Redis 作者的文章中,重点有 3 个:
1)解释时钟问题。 Redis 作者表示,Redlock 并不需要完全一致的时钟,只需要大体一致就可以了,允许有「误差」。 (不晓得哦,前面不是要计算时间吗?)
2)解释网络延迟、GC 问题 Redis 作者强调:如果在 1-3 发生了网络延迟、进程 GC 等耗时长的异常情况,那在第 3 步 T2 - T1,是可以检测出来的,如果超出了锁设置的过期时间,那这时就认为加锁会失败,之后释放所有节点的锁就好了!
(但是如果是锁已经拿到手上了呢,拿到手之后GC)
- 质疑 fencing token 机制 第一,这个方案必须要求要操作的「共享资源服务器」有拒绝「旧 token」的能力。 第二,退一步讲,即使 Redlock 没有提供 fencing token 的能力,但 Redlock 已经提供了随机值(就是前面讲的 UUID),利用这个随机值,也可以达到与 fencing token 同样的效果。
我的思考
我想了好久,从上次接触 redis 分布式锁开始,因为没有思考清楚,我的毕设一直处于迟缓进度状态。
1、从生产实际出发。甘瓜苦蒂,天下物无全美。我们为什么要要求一套方案解决所有的问题呢?我只要这一套方案解决我的问题就行了,别人想借鉴,就借鉴呗,但不保证和他的需求百分百契合。
2、所以我的毕设决定采用:
代码语言:javascript复制setnx px
daemon
fencing token
lua
的方案,有几个点: 1、fencing token 实现方案:高并发下唯一 ID 生成方案 2、daemon 有限续命,如果占据锁的实例挂了,不会无休止的续命导致锁一直无法释放。且续命到达一定时长将写入日志,作为慢业务进行优化处理。 3、对于主从替换导致的分布式锁失效,做成 集群 主从,降低单节点被打崩的风险,也降低单节点承载的业务量。万一真被打崩了,不至于全军覆没。且由于 fencing token 的存在,可以将损失控制在很低的比率。 毕竟我的毕设里面实时数据快速变化的场景也就那么一两秒,平常流量不会那么大。