分布式锁是一种在分布式系统中实现同步机制的技术。它允许多个进程或节点在访问共享资源时进行同步,以确保它们按照预期的顺序执行。
这篇文章使用Redis来分布式锁,通俗的来说,分布式锁本质上要实现的目标就是在Redis里面占一个"茅坑",当别的进程也要来占时,发现已经有人蹲在那里了,就只好放弃或者稍后重试。
接下来我们来循序渐进的实现一个成功的分布式锁。
Redis分布式锁一般使用setnx(setifnotexists)指令,只允许被一个客户端占坑。先来先占,用完了,再调用del指令释放"茅坑"。
setnx lock:codehole true
...do something critical ...
del lock:codehole
简单的使用setnx指令肯定会出现一些问题
问题一:如果逻辑执行到中间出现异常了,可能会导致del指令没有被调用,这样就会陷入死锁,锁永远得不到释放。
我们首先想到的肯定是:“加一个过期时间就行了”。那我们接着往下看。
给锁加上过期时间后,如果程序执行到中间出现异常或超时了,到了过期时间后就自动释放锁。
setnx lock:codehole true
expire lock:codehole 5
... do something critical ...
del lock:codehole
问题二:如果在setnx和expire之间服务器进程突然挂掉了,可能是因为机器掉电或者是被人杀掉的,就会导致expire得不到执行,也会造成死锁。
解决方案: 使用Redis中setnx和expire组合原子命令。
为了解决这个问题,Redis在2.8版本中加入了set指令的扩展参数,使得setnx和expire指令可以一起执行,解决了这个问题。
set lock:codehole true ex 5 nx
... do something crutical ...
del lock:codehole
问题三:释放锁错乱问题,当前锁可能释放的不是自己的锁。
如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候锁过期了,第二个线程重新持有了这把锁,但是紧接着第一个线程执行完了业务逻辑,就把锁给释放了。
场景:如果业务逻辑的执行时间是7s。执行流程如下
index1业务逻辑没执行完,3秒后锁被自动释放。
index2获取到锁,执行业务逻辑,3秒后锁被自动释放。
index3获取到锁,执行业务逻辑
index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,导致index3的业务只执行1s就被别人释放。
最终等于没锁的情况。
解决方案:获取锁时,设置一个指定的唯一值value(例如:雪花Id),释放前获取这个锁,判断是否是自己的锁。
获取锁时,设置一个自己的唯一值,释放锁之前先判断这个value唯一值是否是自己的锁,如果是自己的锁才可以释放。
set lock:codehole 唯一值 ex 5 nx
... do something crutical ...
释放之前判断唯一值是否是自己加锁前的唯一值
del lock:codehole
问题:判断加删除操作缺乏原子性,也会造成问题。
场景:
index1执行删除时,查询到的lock值确实和uuid相等
index1执行删除前,lock刚好过期时间已到,被redis自动释放,在redis中没有了lock,没有了锁。
index2获取了lock,index2线程获取到了cpu的资源,开始执行方法
index1执行删除,此时会把index2的lock删除。index1 因为已经在方法中了,所以不需要重新上锁。index1有执行的权限。index1已经比较完成了,这个时候,开始执行删除的index2的锁!
解决方案:使用lua脚本,lua脚本可以保证连续多个指令的原子性执行。
使用 Lua 脚本来处理,Lua 脚本可以保证连续多个指令的原子性执行。
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
最终,分布式锁的最终版本就是Redis中setnx 和 Lua脚本了。