布式锁实现的三个核心要素:加锁、解锁、锁超时。
情景:存在多台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)。
非原子性操作
??加锁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/p/11072966.html