实现的三个核心要素:
加锁
setnx(key,1)
当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁;当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败。
解锁
del(key)
释放锁之后,其他线程就可以继续执行setnx命令来获得锁。
锁超时
如果一个得到锁的线程在执行任务的过程中挂掉,来不及显示地释放锁,这块资源将会永远被锁住,别的线程就再也进不来了。
设置超时时间这把锁就在一定时间后自动释放了。
expire(key,30)
伪代码如下
if(setnx(key,1) == 1){
expire(key,30)
try{
do somethind....
}finally{
del(key)
}
}
上面的伪代码存在三个致命问题:
1.setnx和expire的非原子性
节点1的setnx刚执行成功,还未执行expire指令,节点1宕机了。
这样,别的线程就再也无法获得锁了。
setnx指令本身不支持传入超时时间,但是set指令增加了可选参数,伪代码如下:
set(key,1,ex 30,nx)
查看这篇博客 https://blog.csdn.net/ningmengbaby/article/details/92088795
就知道上面指令的意思了。
2.del 误删
节点1成功得到了锁,并且设置超时时间是30秒。但是A执行很慢,过了30秒还没有执行完,这时候锁过期自动释放。线程B得到了锁。
线程A执行完,del释放锁,但此时线程B还么有执行完,线程A实际上删除的就是线程B加的锁。
解决方法:在del释放锁之前做一个判断,验证当前的锁是不是自己加的锁。
具体实现:在加锁的时候把当前的线程ID当做value,并在删除之前验证key对应的value是不是自己的线程ID。
加锁:
String threadId = Thread.currentThread().getId()
set(key threadId,ex 30,nx)
解锁:
if(threadId.equals(redisClient.get(key))){
del(key)
}
但是,判断和释放锁是两个独立操作,不是原子性的。所以这一块可以用Lua脚本来实现。这里因为没有研究过就不阐述了。
3.出现并发的可能性
第二点我们避免了线程A误删掉key的情况,但是同一时间有A,B两个线程在访问代码块,仍然不完美。
解决方法:让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”。
当过去了29秒,线程A还没执行完,这时候守护线程会执行expire指令,为这把锁“续命”20秒。守护线程从第29秒开始执行,每20秒执行一次。
当线程A执行完任务,会显式关掉守护线程。