布式锁实现的三个核心要素:加锁、解锁、锁超时。
一、Redis实现分布式锁基本原理
1.1、Redis具体实现
情景:存在多台JVM需要同时对某一商品(id)进行操作。
-
加锁:使用setnx命令,伪代码:setnx(id,value)
返回1,说明key不存在,线程抢锁成功;
返回1,说明key已存在,线程抢锁失败。
注意:setnx(id,value)中key为操作商品的id,value值可用于防止误删锁,下文有提到。 -
解锁:使用del命令,伪代码:del(id)
-
锁超时:如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,该资源将会被永远占用,其他线程将无法访问。
可以使用expire为key设置一个超时时间,与setnx命令一起执行(setnx不支持超时参数),用以保证即使未被显式释放,该锁也可在一定时间后自动释放。伪代码:expire(key, 30)。
1.2、上述实现存在的问题
-
非原子性操作
加锁setnx和锁超时expire两个命令未非原子性操作,当执行加锁setnx后,若因网络或客户端问题锁超时expire命令未成功执行时,锁将无法被释放。
解决方案:
使用set命令取代setnx和expire命令。setnx本身不支持设置超时时间。在Redis 2.6.12以上版本为set指令增加了可选参数,伪代码:set(key, value, expire)。 -
误删锁
设想如下情形:
(1)JVM1使用set(001, 002, 30)成功获取锁,并设置超时时间为30s;
(2)JVM1开始数据处理,处理时间已经超过了30s...
(3)服务器检测到(001, 002, 30)数据超时,将自动执行del进行数据删除,此时JVM1还在数据处理...
(4)此时,JVM2使用set(001, 002, 30)成功获取锁,并设置超时时间为30s;
(5)JVM2开始数据处理。与此同时,JVM1处理完成,操作提交后,根据商品id001,执行了del;
到此,JVM1成功误删了JVM2的锁。
解决方案:
del数据之前,增加锁判断机制:判断要删除的锁是否属于本线程。操作流程:
(1)加锁:set(id, threadId,expire),其中value为当前线程ID;
(2)解锁:执行del命令时,根据id和threadId数据判断该锁是否仍属于本线程。是,则删除。 -
并发问题
基于误删锁的前提下,由于我们无法确定程序成功处理完成数据的具体时间,这就为超时时间的设置提出了难题。设置时间过长、过短都将影响程序并发的效率。
解决方案:JVM1需要自己判断在超时时间内是否完成数据处理,如未完成,应请求延长超时时间。具体操作:
为获取锁的锁的线程开启一个守护线程。当29秒时(或更早),线程A还没执行完,守护线程会执行expire指令,为这把锁“续命”20秒。守护线程从第29秒开始执行,每20秒执行一次。当线程A执行完任务,会显式关掉守护线程。
另一种情况:如果节点1 忽然断电,由于线程A和守护线程在同一个进程,守护线程也会停下。当过了超时时间后,没有守护进程的“续命”,锁将自动释放。
出处: https://www.cnblogs.com/DeepInThought
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。