本质上要实现的目标是在redis
中占一个坑。当别的进程也要来占坑的时候,发现已经有"大萝卜"只好放弃或稍后再试。
一般setnx
指令,只允许一个客户端占坑。先来先占,用完了,在调del
指令释放
setnx lock:codehole true
del lock:codehole
但是有个问题,如果逻辑执行到中间出现异常,可能会导致del
指令没有被调用,这样就会陷入死锁,锁永远得不到释放
于是我们在拿到锁之后,再给锁加上一个过期时间,比如5s
,这样即使中间出了异常也可以保证5s
之后锁会自动释放
setnx lock:codehole true
expire lock:codehole 5
del lock:codehole
在setnx
和expire之间服务器进程突然挂掉,expire得不到执行,也会造成死锁
根源是setnx
和 expire 是两条指令而不是原子指令
redis
2.8版本中,作者加入了set指令的拓展,使setnx
和expire可以一起执行
SET key value [EX seconds] [PX millisecounds] [NX|XX]
EX seconds:设置键的过期时间为second秒
PX millisecounds:设置键的过期时间为millisecounds 毫秒
NX:只在键不存在的时候,才对键进行设置操作
XX:只在键已经存在的时候,才对键进行设置操作
SET操作成功后,返回的是OK,失败返回NIL
set lock:codehole true ex 5 nx
del lock:codehole
超时问题
redis
的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行得太长,以至于超出了锁的超时限制。第一个线程持有的锁过期了,而第二个线程提前获得了锁,导致临界区代码不能得到严格串行执行。
为了避免这个问题,分布式锁不要用于较长时间的任务。如果真的偶尔出现了问题,造成的数据小错乱可能需要人工介入解决。
有一个稍微安全一点的方法是将set指令的value参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后在删除key,这里为了确保当前线程占有的锁不会被其他线程释放,除非这个锁时因为过期了而被服务器自动释放的。
但是匹配value和删除key不是一个原子操作,需要用lua脚本来处理,因为lua脚本可以保证连续多个指令的原子性执行
缺点:如果真的超时了,当前线程的逻辑没有执行完,其他线程也会乘虚而入。
可重入性
可重入性是指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程的多次加锁,那么这个锁是可重入性的
锁冲突处理
-
直接抛出特定类型的异常
由用户去处理,是否重新发起新的请求或者放弃
-
sleep
sleep会阻塞当前的消息处理线程,导致队列的后续消息处理出现延迟。如果碰撞的比较频繁或者队列里消息比较多,sleep可能并不合适。如果因为个别死锁的key导致加锁不成功,线程会彻底堵死,导致后续消息永远得不到及时处理
-
延迟队列
适合异步处理的消息,将当前冲突的请求扔到另一个队列延后处理以避免冲突