(十四)Redis实现分布式锁方案比较
SETNX 语义
1 | SETNX key value |
命令执行时,如果 ==key== 不存在,则设置 key 值为 value(同set);如果 key 已经存在,则不执行赋值操作。并使用不同的返回值标识。
1 | SET key value [expiration EX seconds|PX milliseconds] [NX|XX] |
==NX== - 仅在 key 不存在时执行赋值操作。命令描述文档 而如下文所述,通过SET的NX选项使用,可同时使用其它选项,如EX/PX设置超时时间,是更好的方式。
SETNX 实现分布式锁
方案1:SETNX + delete
伪代码如下:1
2
3setnx lock_a random_value
// do sth
delete lock_a
此实现方式的问题在于:一旦服务获取锁之后,因某种原因挂掉,则锁一直无法自动释放。从而导致死锁。
方案2:SETNX + SETEX
伪代码如下:1
2
3
4setnx lock_a random_value
setex lock_a 10 random_value // 10s超时
// do sth
delete lock_a
按需设置超时时间。此方案解决了方案1死锁的问题,但同时引入了新的死锁问题: 如果setnx之后,setex 之前服务挂掉,会陷入死锁。 根本原因为 setnx/setex 分为了两个步骤,非原子操作。
方案3:SET NX PX
伪代码如下:1
2
3SET lock_a random_value NX PX 10000 // 10s超时
// do sth
delete lock_a
此方案通过 set 的 NX/PX 选项,将加锁、设置超时两个步骤合并为一个原子操作,从而解决方案1、2的问题。(PX与EX选项的语义相同,差异仅在单位。)
此方案目前大多数 sdk、redis 部署方案都支持,因此是推荐使用的方式。
但此方案也有如下问题:
如果锁被错误的释放(如超时),或被错误的抢占,或因redis问题等导致锁丢失,无法很快的感知到。
SET key randomvalue NX PX
方案4在3的基础上,增加对 value 的检查,只解除自己加的锁。
类似于 CAS,不过是 compare-and-delete。
此方案 redis 原生命令不支持,为保证原子性,需要通过lua脚本实现:。
伪代码如下:1
2
3SET lock_a random_value NX PX 10000
// do sth
eval "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock_a random_value
此方案更严谨:即使因为某些异常导致锁被错误的抢占,也能部分保证锁的正确释放。并且在释放锁时能检测到锁是否被错误抢占、错误释放,从而进行特殊处理。
注意事项
超时时间
从上述描述可看出,超时时间是一个比较重要的变量:
超时时间不能太短,否则在任务执行完成前就自动释放了锁,导致资源暴露在锁保护之外。
超时时间不能太长,否则会导致意外死锁后长时间的等待。除非人为接入处理。
因此建议是根据任务内容,合理衡量超时时间,将超时时间设置为任务内容的几倍即可。
如果实在无法确定而又要求比较严格,可以采用定期 setex/expire 更新超时时间实现。
重试
如果拿不到锁,建议根据任务性质、业务形式进行轮询等待。
等待次数需要参考任务执行时间。
与redis事务的比较
setnx 使用更为灵活方案。multi/exec 的事务实现形式更为复杂。
且部分redis集群方案(如codis),不支持multi/exec 事务。