[TOC]
参考
摘抄自 使用redis实现分布式锁
1. 分布式锁的使用场景
第一个是正确性,这个众人皆知。就像Java里的synchronize,就是用来保证多线程并发场景下,程序的正确性。在redis的场合下,并发访问的单位,不再是线程,而是进程。
举个例子,一个文件系统,为了提高性能,部署了三台文件服务器。当服务器A在修改文件A的时候,其他服务器就不能对文件A进行修改,否则A的修改就会被覆盖掉。
锁还有第二个用处——效率。比如应用A有一个耗时的统计任务,每天凌晨两点,定时执行,这时我们给应用A部署了三台机器,如果不加锁,那么每天凌晨两点一到,这三台机器就都会去执行这个很耗时的统计任务,而实际上,我们最后只需要一份统计结果。
这时候,就可以在定时任务开始前,先去获取锁,获取到锁的,执行统计任务,获取不到的,就直接结束。
2. 获取锁
要怎么在redis里获取一把锁呢?貌似很简单,执行set命令就好了,还是上面文件系统的例子,比如你想修改文件id是9527的文件,那就往redis里,添加一个key为file:9527,value为任意字符串的值即可:
set file:9527 ${random_value}
set成功了,就说明获取到锁。
这样可以吗?很明显不行,set方法默认是会覆盖的,也就是说,就算file:9527已经有值了,set还是可以成功,这样锁就起不到互斥的作用。
那在set之前,先用get判断一下,如果是null,再去set?也不行,原因很简单,get和set都在客户端执行,不具有原子性。
要实现原子性,唯一的办法,就是只给redis发送一条命令,来完成获取锁的动作。
于是就有了下面这条命令:
set file:9527 ${random_value} NX
NX = If Not Existed 如果不存在,才执行set。
完美了吗?非也,这个值没有设置过期时间,如果后面获得锁的客户端,因为挂掉了,或者其他原因,没有释放锁,那其他进程也都获取不到锁了,结果就是死锁。
所以有了终极版的获取锁命令:
set file:9527 ${random_value} NX EX ${timeout}
使用EX参数,可以设置过期时间,单位是秒,另一个参数PX,也可以设置过期时间,单位是毫秒。
2. 释放锁
好,最后再来看看释放锁。
有人说,释放锁,简单,直接del:
del file:9527
有问题吗?当然有,这会把别人的锁给释放掉。
举个例子:
A拿到了锁,过期时间5s
5s过去了,A还没释放锁,也许是发生了GC,也许是某个耗时操作
锁过期了,B抢到了锁
A缓过神来了,以为锁还是自己的,执行del file:9527
C抢到了锁,也进来了
B看看屋里的C,有看看刚出门的A,对着A吼了一句:尼玛,你干嘛把我的锁释放了
所以,为了防止把别人的锁释放了,必须检查一下,当前的value是不是自己设置进去的value,如果不是,就说明锁不是自己的了,不能释放。
显然,这个过程,如果放在客户端做,就又不满足原子性了,只能整在一起,一次性让redis server执行完。
这下redis可没有一条命令,可以做这么多事情的,好在redis提供了lua脚本的调用方式,只需使用eval命令调用以下脚本即可:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
3. Redlock算法
3.1. 背景
了解完如何释放锁,再加上之前的获取锁,我们似乎已经可以用redis来实现分布式锁了。
但是,一如既往,问自己一句,完美了吗?没有漏洞了?嗯,很明显不是,上面讲的算法,都有一个前提:只有一台redis实例。
而生产环境里,我们是不可能只部署一个实例的,至少,我们也是主从的架构。redis的数据同步,不是强一致性的,毕竟作为一个缓存,要保证读写性能。
如果A往Master放入了一把锁,然后再数据同步到Slave之前,Master crash,Slave被提拔为Master,这时候Master上面就没有锁了,这样其他进程也可以拿到锁,违法了锁的互斥性。
3.2. 原理
针对Redis集群架构,redis的作者antirez提出了Redlock算法,来实现集群架构下的分布式锁。
Redlock算法并不复杂,我们先简单描述一下,假设我们Redis分片下,有三个Master的节点,这三个Master,又各自有一个Slave。
好,现在客户端想获取一把分布式锁:
记下开始获取锁的时间 startTime
按照A->B->C的顺序,依次向这三台Master发送获取锁的命令。客户端在等待每台Master回响应时,都有超时时间 timeout。举个例子,客户端向A发送获取锁的命令,在等了 timeout 时间之后,都没收到响应,就会认为获取锁失败,继续尝试获取下一把锁
如果获取到超过半数的锁,也就是 3/2+1 = 2把锁,这时候还没完,要记下当前时间 endTime
计算拿到这些锁花费的时间 costTime = endTime - startTime,如果costTime小于锁的过期时间 expireTime,则认为获取锁成功
如果获取不到超过一半的锁,或者拿到超过一半的锁时,计算出 costTime >= expireTime,这两种情况下,都视为获取锁失败
如果获取锁失败,需要向全部Master节点,都发生释放锁的命令,也就是那段Lua脚本
当然这个Redlock算法也并不是万能的,也会有缺陷,我也在思考在哪些场景下会有问题。但是在目前绝大情况下来说,Redlock已经足够用了。