28.Redis的分布式锁
我们说了除了原子操作,Reids还可以通过加锁的方式,来控制并发写操作对共享数据的修改,从而保证数据的正确性
在分布式的系统中,当有多个客户端需要获取到锁的时候,需要使用分布式锁,锁是保存在一个共享存储系统中,可以被多个客户端共享访问和获取
Redis则满足上述那些要求,本身就是一个共享存储系统,可以使用来保存分布式锁,并且支持高并发的操作场景
我们这章将锁如何实现到单机锁最后到集群锁的思路来讲解
1.对于锁的实现
单机上运行的多线程程序,锁本身可以使用一个变量表示
变量为0的时候,表示没有线程获取锁
变量为1的时候,表示已经被线程获取到了
比如AQS中加锁,就是检查锁变量是否为0,如果是0的话,就把锁的变量设置为1,表示获取到锁,如果不是0,就返回错误信息,表示加锁失败
释放锁就是重新将锁设置为0,以便其他线程获取到锁
那么分布式锁也是用一个共享存储系统中维护,多个客户端通过访问这个系统来访问锁的变量
那么就对应着有两个问题,
其一,分布式锁的加锁和释放,涉及了多个操作,故需要保证这些锁操作的原子性
其二,共享存储系统保存锁变量,无论是应用还是共享存储系统,都需要考虑宕机的可能性
对于Redis实现的分布式锁,Redis可以使用键值对来保存锁变量
我们需要赋予锁变量一个变量名,将这个变量名作为键,然后锁的主要操作就是操作这个键对应的值
Redis可以使用lock_key作为key保存锁,对应的初始值为0
然后加锁过程中
A和C同时请求加锁,因为Redis使用单线程处理,所以Redis会串行处理这个请求
Redis处理了A,会将lock_key的value设置为1,表示加锁了,然后Redis处理客户端C的请求,发现已经值为1了,返回加锁失败的信息
加锁的整体流程对应到Redis中,就是首先获取锁的值,然后判断锁的值,然后将变量值设置为0
整体的流程可以考虑使用Lua脚本保证原子性,如果只是使用Redis命令,其实也可以做到,
利用了SETNX命令,设置的键值对不存在,会设置这个值,而且还可以加上过期时间
但是直接使用SETNX存在一个问题,就是假设客户端A执行了SETNX命令加锁了,如果有客户端B直接DEL这个变量释放锁,那么可能让第三者获取到锁,导致共享数据被修改
所以我们不能直接DEL,或者说需要在释放锁的时候判断是不是自己加的锁
于是乎,可以考虑修改SETNX命令加锁的时候锁的变量只为1或者0,表示释放加锁成功,1和0只有两种状态,无法表示是哪个客户端进行的加锁,所以我们可以考虑每个客户端给锁加上一个唯一的值,释放锁的时候,只有当前的变量的值和自己唯一标识相同,才可以释放锁
那么在加锁的时候我们可以生成一个随机字符串或者利用应用的唯一标识进行加锁\
SET lock_key unique_value NX PX 1000
上面利用唯一标识加了锁
对应的释放流程,就需要读取设置的值,然后判断值是否等于,最后进行释放
这么一个流程,我们可以用Lua脚本去执行,保证其原子性
//释放锁 比较unique_value是否相等,避免误释放
if redis.call(“get”,KEYS[1]) == ARGV[1] then return redis.call(“del”,KEYS[1]) else return 0 end |
那么执行的代码如下
redis-cli –eval unlock.script lock_key , unique_value
那么这就是Redis单机的实现
如果需要多个Redis节点实现分布式锁,可以利用Redlock
就是客户端和多个Redis实例通信请求加锁,能和半数以上的实例加锁成功,那么就认为获得锁了,不然就是加锁失败
整体流程就是获取到当前时间
然后依次的向N个实例执行加锁操作
然后加锁操作超过半数,认为加锁成功,就需要计算整体的耗时
需要这个耗时没有超过锁的有效时间,满足了这两个条件之后,就可以重新计算这把锁的有效时间,方便释放锁
然后就可以使用了,一般复杂的,不近人情的加锁方式
总结一下
Redis作为一个共享存储系统,可以用来实现分布式锁
单个Redis实现分布式锁的时候,需要注意,我们在加锁ode时候
需要读取锁变量,检查锁变量,设置锁变量,三个操作,这里我们使用了SET NX来进行加锁
而且在其中我们需要设置 过期时间,避免拿到锁之后发生异常,导致锁没法释放,所以我们加上了EX/PX选项,设置过期时间
锁的变量的值需要能够区分不同客户端的加锁操作,避免释放锁的时候,出现释放错误的问题
如果是集群的分布式锁,锁变量由多个实例维护,即使有实例发生了故障,锁变量依然会存在,帮助客户端完成锁操作
最后一问
SET命令带上NX和EX/PX进行加锁,那么不一口气做呢?
首先回答下这个问题,如果不保证原子性,那么实例宕机了,岂不是没有别人能拿到锁了,这是大问题
第二个问题,按照老师说的Redlock,达到半数以上,那么由一个问题,就是出现集群实例宕机一部分,导致其他客户端也达到了获取锁的数量(即在集群半数上加锁成功),那时候怎么办