Redis分布式锁实现详解
分布式锁
分布式锁是什么
分布式锁就是分布式场景下的锁,比如多台不同机器上的进程,去竞争同一项资源,这个时候就需要加锁
分布式锁特性
- 互斥性:锁的目的是获取资源的使用权,所以只让一个竞争者持有锁,这一点要尽可能保证
- 安全性:避免锁因为异常永远不被释放。当一个竞争者在持有锁期间内,由于意外崩溃而导致未能主动解锁,其持有的锁也能够被兜底释放,并保证后续其它竞争者也能加锁
- 对称性:同一个锁,加锁和解锁必须是同一个竞争者。不能把其他竞争者持有的锁给释放了
- 可靠性:需要有一定程度的异常处理能力、容灾能力。
常用实现方式
- 分布式锁,一般会依托第三方组件实现
- 利用 redis 实现是用的最多的一种
最简化版本
实现方式
直接用Redis的setnx命令,这个命令的语法是:setnx key value
如果key不存在,则会将key设置为value,并返回1
如果key存在,不会有任务影响,返回0
基于这个特性,我们就可以用setnx实现加锁的目的:通过setnx加锁,加锁之后其他服务无法加锁,用完之后,再通过delete解锁,
流程图

升级 1:支持过期时间
最简化版本有一个问题
如果获取锁的服务挂掉了,那么锁就一直得不到释放,就像石沉大海,杳无音信
所以,我们需要一个超时来兜底。
设置过期时间
Redis中有expire命令,用来设置一个key的超时时间。
但是setnx和expire不具备原子性,如果setnx获取锁之后,服务挂掉,依旧是泥牛入海。
原子性问题
nx表示具备setnx 特性,ex表示增加了过期时间,最后一个参数就是过期时间的值
流程图

升级 2:超时 + 锁归属
升级 1 存在的问题:可能服务 A 会释放掉服务 B
想一下如下场景:
服务A获取了锁,由于业务流程比较长,或者网络延迟、GC卡顿等原因,导致锁过期,而业务还会继续进行。
这时候,业务B已经拿到了锁,准备去执行
这个时候服务A恢复过来并做完了业务,就会释放锁,而B却还在继续执行。
在真实的分布式场景中,可能存在几十个竞争者,那么上述情况发生概率就很高,导致同一份资源频繁被不同竞争者同时访问,分布式锁也就失去了意义。
🌰想一下如下场景:基于这个场景,可以发现,问题关键在于,竞争者可以释放其他人的锁。 那么在异常情况下,就会出现问题,所以我们可以进一步给出解决方案: 🌰分布式锁需要满足谁申请谁释放原则,不能释放别人的锁,也就是说,分布式锁,是要有归属的。
给锁加上 owner

升级 3:引入 Lua
升级 2 存在的问题
升级 2 的流程:竞争者获取锁执行任务,执行完毕后,检查锁是不是自己的,最后再进行释放
这一通下操作都不是原子化的,可能锁获取的时候还是自己的,但是删除的时候已经是别人的了
如何理解:锁获取的时候还是自己的,但是删除的时候已经是别人的了
- 旧客户端删除前检查锁,显示旧客户端是 owner
- 旧客户端准备释放锁,恰好这时候锁过期了,相当于锁被自行释放了
- 此时,新客户端来获取到了这个锁,owner 是新客户端
- 旧客户端执行释放操作,把新客户端的锁释放了
Redis+Lua
Redis还有个特性,专门整合原子操作 —— Lua

升级 3 满足:对称性,安全性,互斥性
升级 4:可靠性的保证
前面的内容,基本是基于单机考虑的,如果Redis挂掉了,那锁就不能获取了。
这个问题该如何解决呢? 一般来说,有两种方法:
主从容灾- 多级部署。
1. 主从容灾
最简单的一种方式,就是为Redis配置从节点,当主节点挂了,用从节点顶包。

但是主从切换,需要人工参与,会提高人力成本。
不过Redis已经有成熟的解决方案,也就是哨兵模式,可以灵活自动切换,不再需要人工介入。
哨兵模式

2. 多机部署
如果对一致性的要求高一些,可以尝试多机部署,比如Redis的RedLock,
RedLcok
大概的思路就是多个机器,通常是奇数个,达到一半以上同意加锁才算加锁成功,这样,可靠性会向ETCD靠近。还不了解 etcd?一文带你快速入门(万字长文)-腾讯云开发者社区-腾讯云
现在假设有5个Redis主节点,基本保证它们不会同时宕掉,获取锁和释放锁的过程中,客户端会执行以下操作:
- 向5个Redis申请加锁
- 只要超过一半,也就是3个Redis返回成功,那么就是获取到了锁。 如果超过一半失败,需要向每个Redis发送解锁命令
- 由于向5个Redis发送请求,会有一定时耗,所以锁剩余持有时间,需要减去请求时间 这个可以作为判断依据,如果剩余时间已经为0,那么也是获取锁失败
- 使用完成之后,向5个Redis发送解锁请求。

这种模式的好处在于,如果挂了2台Redis,整个集群还是可用的,给了运维更多时间来修复。
另外,单点Redis的所有手段,这种多机模式都可以使用
比如为每个节点配置哨兵模式,由于加锁是一半以上同意就成功,那么如果单个节点进行了主从切换,单个节点数据的丢失,就不会让锁失效了。
3. RedLock 可靠性探索
分布式系统的三困境:NPC
- N:Network Delay(网络延迟)
- P:Process Pause(进程暂停)
- C:Clock Drift(时钟漂移)
Network Delay
当分布式锁获得返回包的时间过长,此时可能虽然加锁成功,但是已经时过境迁,锁可能很快过期。
RedLock算做了些考量,也就是前面所说的锁剩余持有时间,需要减去请求时间,如此一来,就可以一定程度解决网络延迟的问题
Process Pause
比如发生GC:获取锁之后GC了,处于GC执行中,然后锁超时。

其它锁想获取,这种情况几乎无解。
这时候GC回来了,那么两个进程就获取到了同一个分布式锁。(GC 回来,以为自己还持有锁,继续做任务)

也许会说,在GC回来之后,可以再去查一次啊?这里有两个问题
- 你怎么知道GC回来了? 这个可以在做业务之前,通过时间,进行一个粗略判断
- 如果你判断的时候是ok的,但是判断完才开始 GC 呢? 这点RedLock是无法解决的。
Clock Drift
如果竞争者A,获得了RedLock,在5台分布式机器上都加上锁。
为了方便分析,直接假设5台机器都发生了时钟漂移,锁瞬间过期了。
这时候竞争者B拿到了锁,此时A和B拿到了相同的执行权限。
根据上述的分析可以看出,RedLock也不能扛住NPC的挑战,因此,单单从分布式锁本身出发,完全可靠是不可能的。
要实现一个相对可靠的分布式锁机制,还是需要和业务的配合,业务本身要幂等可重入,这样的设计可以省却很多麻烦