再探 redis 分布式锁

Posted 看,未来

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了再探 redis 分布式锁相关的知识,希望对你有一定的参考价值。

文章目录

前言

“分布式锁”这个话题在程序界有很大的关注量,引发了不少讨论。关于分布式锁有很多实现的方案,本文就讲基于 Redis 实现的分布式锁。一些基本的东西我就直接带过吧。

问:为什么需要分布式锁?
答:以前为什么需要互斥锁?

Redis 分布式锁的演进:

1、setnx
存在问题:若上锁的实例还没解锁就挂了,就死锁了。
解决方案:为锁设置一个过期时间。
2、setnx px,为锁设置一个过期时间。
存在问题:锁过期了,但是任务还没完成,锁就被释放了。当上锁者完成任务后,容易释放别的业务上的锁。

解决方案:可以采用看门狗进程,当锁要过期了就给它延时;也可以为锁设置一个token,只有token匹配上了才能解锁。
3、setnx key token px time
这里不采用看门狗进程的解法,具体可自己思考。

存在问题:若在解锁做 token 判断成功之后,解锁线程出现了调度,或延时,当真正操作 del 的时候锁已经过期了,则将把别的业务上的锁给解锁掉。

解决方案:将解锁过程写入 lua 脚本,将解锁过程原子化。调用 lua 脚本解锁。

lua 脚本如下:

--判断锁是自己的才释放
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 个实例。

问:不做主从,那万一某个库宕机了,数据不就都丢了吗?
答:丢就丢了呗,本来就不是用来存数据的。

具体流程如下:

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。

N:Network Delay,网络延迟
P:Process Pause,进程暂停(GC)
C:Clock Drift,时钟漂移

Martin 用一个进程暂停(GC)的例子,指出了 Redlock 安全性问题:

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、假设时钟正确是不合理的。

客户端C1获得了对节点A、B、c的锁定,由于网络问题,法到达节点D和节点E

节点C上的时钟向前跳,导致锁提前过期

客户端C2在节点C、D、E上获得锁定,由于网络问题,无法到达A和B

客户端C1和客户端C2现在都认为他们自己持有锁

前跳应该是不至于了,不过系统时钟是存在误差的,可以看我前面发的那篇:
计算机时钟是如何运行的?


4、提出 fencing token 的方案,保证正确性

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)

  1. 质疑 fencing token 机制
    第一,这个方案必须要求要操作的「共享资源服务器」有拒绝「旧 token」的能力。
    第二,退一步讲,即使 Redlock 没有提供 fencing token 的能力,但 Redlock 已经提供了随机值(就是前面讲的 UUID),利用这个随机值,也可以达到与 fencing token 同样的效果。

我的思考

我想了好久,从上次接触 redis 分布式锁开始,因为没有思考清楚,我的毕设一直处于迟缓进度状态。

1、从生产实际出发。甘瓜苦蒂,天下物无全美。我们为什么要要求一套方案解决所有的问题呢?我只要这一套方案解决我的问题就行了,别人想借鉴,就借鉴呗,但不保证和他的需求百分百契合。

2、所以我的毕设决定采用:

setnx px 
	+
daemon
	+
fencing token
	+ 
lua

的方案,有几个点:
1、fencing token 实现方案:高并发下唯一 ID 生成方案
2、daemon 有限续命,如果占据锁的实例挂了,不会无休止的续命导致锁一直无法释放。且续命到达一定时长将写入日志,作为慢业务进行优化处理。
3、对于主从替换导致的分布式锁失效,做成 集群 + 主从,降低单节点被打崩的风险,也降低单节点承载的业务量。万一真被打崩了,不至于全军覆没。且由于 fencing token 的存在,可以将损失控制在很低的比率。
毕竟我的毕设里面实时数据快速变化的场景也就那么一两秒,平常流量不会那么大。

以上是关于再探 redis 分布式锁的主要内容,如果未能解决你的问题,请参考以下文章

基于redis的分布式锁详解

Redis专题(分布式锁)

重学springboot系列之集群多节点应用session共享,redis分布式锁

如何用redis正确实现分布式锁?

Redis分布式锁

分布式锁用zookeeper还是redis好