Redis 分布式锁
Redis分布式锁
分布式锁
分布式锁的本质是要是实现在Redis里面占一个坑,当别的进程要占用时,发现被占用,就只好放弃或者稍后再试。
Redis一般用setnx(set if not exists)指令,先来先占,用完在调用del指令释放掉。
但是有个问题,如果执行过程中出现异常,可能会导致del指令没有被调用,这样会陷入死锁,锁永远得不到释放。于是我们可以在拿到锁后,给锁加一个过期时间,可以保证中间出现异常后锁也能自动释放。
但是上面逻辑还是有个问题,如果在setnx和expire之间服务器进程突然挂掉,可能因为机器断电或者是被人为杀掉进程,就会导致expire得不到执行,也会造成死锁。
这个问题的根源在于setnx和expire不是原子指令,如果两条指令可以一起执行就不会出现问题了。或许会考虑到使用Redis事务执行。但是这里不行,因为expire依赖setnx执行结果,如果setnx没有抢到锁,expire不该执行。事务中没有if-else分支逻辑,事务的特点是一口气执行,要么全部执行,要么一个都不执行。
在Redis2.8版本中,作者加入了set指令的扩展参数,是的setnx和expire指令可以一起执行。
redis 127.0.0.1:6379> SET key-with-expire-and-NX "hello" EX 10086 NX
OK
超时问题
Redis分布式锁不能解决超时问题,如果在加锁和释放锁之间逻辑执行太长,以至于超出锁的超时时间,就会出现问题。因为这时候锁过期了,第二个线程重新持有了这把锁,但是紧接着第一个线程执行完业务逻辑,把锁释放了,第三个线程就会在第二个线程逻辑执行完之间拿到锁。
为了避免这个问题,Redis分布式锁尽量不要用于较长任务,如果真的偶尔出现,数据出现小波错乱可能需要人工介入。
有一个更为安全的方案就是为set指令的value参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后在删除key,但是匹配value和删除key不是原子操作,Redis也没有提供类似这样的指令,这时需要Lua脚本处理了,因为Lua脚本可以保证多个指令的原子性执行。
可重入性
可重入性指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一线程的多次加锁,那么这个锁就是可重入性的。Redis分布式锁如果需要支持可重入,则需要对客户端的set方法进行包装,Python使用线程的Threadlocal变量存储当前持有锁的计数。
锁冲突处理
客户端在处理请求时加锁没有加锁成功怎么办。一般有3种策略来处理加锁失败:
1、直接抛出异常,通知用户稍后重试
2、sleep一会后重试
3、将请求转移至延时队列,稍后重试