redis除了是很强大的存储/缓存工具之外,还可以实现分布式锁。
下面我们来看看到底怎么样用redis来实现分布式锁。
为什么要使用锁?
第一个是正确性,这个众人皆知。就像Java里的synchronize,就是用来保证多线程并发场景下,程序的正确性。在redis的场合下,并发访问的单位,不再是线程,而是进程。
举个例子,一个文件系统,为了提高性能,部署了三台文件服务器。当服务器A在修改文件A的时候,其他服务器就不能对文件A进行修改,否则A的修改就会被覆盖掉。
锁还有第二个用处——效率。比如应用A有一个耗时的统计任务,每天凌晨两点,定时执行,这时我们给应用A部署了三台机器,如果不加锁,那么每天凌晨两点一到,这三台机器就都会去执行这个很耗时的统计任务,而实际上,我们最后只需要一份统计结果。
这时候,就可以在定时任务开始前,先去获取锁,获取到锁的,执行统计任务,获取不到的,就直接结束。
分布式锁和本地锁的区别是什么?
单机,并发的单位是线程,分布式,并发的单位是多进程。并发单位的等级上去了,锁的等级自然也得上去。以前锁是进程自己的,进程下的线程都看这个锁的眼色行事,谁拿到锁,谁才可以放行。进程外面还有别的进程,你要跟别人合作,就不能光看着自己了,得有一个大家都看得到的,光明正大的地方,来放这把锁。
获取锁
要怎么在redis里获取一把锁呢?貌似很简单,执行set命令就好了,还是上面文件系统的例子,比如你想修改文件id是9527的文件,那就往redis里,添加一个key为file:9527,value为任意字符串的值即可:
1
|
|
set成功了,就说明获取到锁。
这样可以吗?很明显不行,set方法默认是会覆盖的,也就是说,就算file:9527已经有值了,set还是可以成功,这样锁就起不到互斥的作用。
那在set之前,先用get判断一下,如果是null,再去set?也不行,原因很简单,get和set都在客户端执行,不具有原子性。
要实现原子性,唯一的办法,就是只给redis发送一条命令,来完成获取锁的动作。
于是就有了下面这条命令:
1
|
|
NX = If Not Existed 如果不存在,才执行set。
完美了吗?非也,这个值没有设置过期时间,如果后面获得锁的客户端,因为挂掉了,或者其他原因,没有释放锁,那其他进程也都获取不到锁了,结果就是死锁。
所以有了终极版的获取锁命令:
1
|
|
使用EX参数,可以设置过期时间,单位是秒,另一个参数PX,也可以设置过期时间,单位是毫秒。
释放锁
好,最后再来看看释放锁。
有人说,释放锁,简单,直接del:
1
|
|
有问题吗?当然有,这会把别人的锁给释放掉。
举个例子:
1 2 3 4 5 6 |
|
所以,为了防止把别人的锁释放了,必须检查一下,当前的value是不是自己设置进去的value,如果不是,就说明锁不是自己的了,不能释放。
显然,这个过程,如果放在客户端做,就又不满足原子性了,只能整在一起,一次性让redis server执行完。
这下redis可没有一条命令,可以做这么多事情的,好在redis提供了lua脚本的调用方式,只需使用eval命令调用以下脚本即可:
1 2 3 4 5 |
|
其实还有问题
了解完如何释放锁,再加上之前的获取锁,我们似乎已经可以用redis来实现分布式锁了。
但是,一如既往,问自己一句,完美了吗?没有漏洞了?嗯,很明显不是,上面讲的算法,都有一个前提:只有一台redis实例。
而生产环境里,我们是不可能只部署一个实例的,至少,我们也是主从的架构。redis的数据同步,不是强一致性的,毕竟作为一个缓存,要保证读写性能。
如果A往Master放入了一把锁,然后再数据同步到Slave之前,Master crash,Slave被提拔为Master,这时候Master上面就没有锁了,这样其他进程也可以拿到锁,违法了锁的互斥性。
如何解决这个问题?
Redlock算法
针对Redis集群架构,redis的作者antirez提出了Redlock算法,来实现集群架构下的分布式锁。
Redlock算法并不复杂,我们先简单描述一下,假设我们Redis分片下,有三个Master的节点,这三个Master,又各自有一个Slave。
好,现在客户端想获取一把分布式锁:
1 2 3 4 5 6 |
|
当然这个Redlock算法也并不是万能的,也会有缺陷,我也在思考在哪些场景下会有这样的问题。但是在目前绝大情况下来说,Redlock已经足够用了。