本文主要介绍通过redis实现分布式锁。
目录
- 一、分布式锁
- 二、基于单个redis节点的分布式锁
- 2.2.1 redis 命令支持-加锁-setnx
- 2.2.2 redis 命令支持-释放锁-del
- 2.2.3 setnx和del的组合的问题
- 2.2.4 setnx和del的问题解决
- 2.1 示意图
- 2.2 redis命令支持
- 2.3 单节点redis锁的问题
- 三、基于多个redis节点的分布式锁
- 四、选择
在分布式系统中,当有多个客户端(跨进程,机器)需要获取锁时候,就需要分布式锁,这个锁保存在一个共享的存储系统中。
redis就是一个可以被多个客户端共享访问的存储系统,可以用来保存分布式锁,并且redis支持数万的并非操作,读写性能高,可以适应高并发的锁操作场景。
报文主要讨论两种类型的redis锁实现:
- 基于单个redis节点的分布式锁
- 基于多个redis节点的分布式锁(共享存储高可靠)、
背景
- 锁变量名:lock_key。
- value=1:锁被持有。
- value=0:锁被释放。
2.1 示意图
加锁示意图
释放锁示意
2.2 redis命令支持
2.2.1 redis 命令支持-加锁-setnx
加锁需要三个操作
- 读取锁变量
- 判断锁变量的值
- 把锁变量设置为1
以上三个操作需要在一个原子操作里面完成,可以通过lua脚本来实现,但是redis提供了命令
setnx 可以用这个命令代替上面的三个操作,同时实现了原子操作。
setnx用于设置键值对的值,在执行时会判断键是否存在,如果不存在,就创建键,同时设置值,如果存在,就不做任何设置。
最终可以通过setnx的返回值来判断锁是否被持有:
- 返回值1,表示拿到锁
- 返回值0,标识锁失败
2.2.2 redis 命令支持-释放锁-del
del命令删除
2.2.3 setnx和del的组合的问题
可以通过setnx和del的组合实现加锁和释放锁的问题,但是setnx和del的组合有两个问题。
setnx有锁永远无法释放的问题,考虑场景,客户端A使用setnx拿到锁以后,在执行自己业务逻辑的时候发送了异常,最终一直没有调用del来释放锁。锁一直被这个客户端A占用着,其他客户端无法拿到锁,着会严重影响业务.
这种问题解决方案就是给setnx给一个默认的过期时间,当业务异常无法释放锁时,通过默认过期时间,redis会自动把这个锁置为无效,间接释放了锁。
del操作最大的问题是因为锁对应的key是已知的,会出现key被误删的case,客户端A获取到了锁,在释放之前被客户端B给删除了,这个时候锁变成了空闲状态,客户端C又拿到了锁,最终的结果就是A和C同时拿到了锁。
这个问题的核心就是只有加锁着本身才允许解除这个锁。可以在锁对应的值上做文章,让锁的值每次在加锁的时候都能唯一被识别(只有使用锁的客户端才能提供),比如加锁客户端所在的客户端专有信息等
每次解锁的时候必须要传入这个唯一标识。- 解锁前先获取锁对应的值
- 拿这个值和传入的唯一标识进行比较,如果一样才进行解锁,不一样,则不允许解锁
- 以上两个操作必须是一个原子操作
2.2.4 setnx和del的问题解决
分析了问题后就可以针对性解决。
setnx-锁无法释放问题:redis提供了SET key value NX PX timeout。功能和setnx一样,但是会在timeout毫秒后自动过期,这就解决了客户端宕机后没有释放的问题。
del-锁被误释放问题:首先按照上面分析的对value进行维一值设定。然后释放锁采用lua脚本实现check和释放功能。
if redis.call("get",KEYS[1]) == ARGV[1] then //check return redis.call("del",KEYS[1]) else return 0 end
KEYS[1]的值是锁的名字,ARGV[1]的值是锁对应的维一值。只有维一值相等,才进行释放。
2.3 单节点redis锁的问题
以上分析了单点redis实现分布式锁的原理以及方法,但是单点redis最大的问题就是单点本身,当出现故障宕机或者网络问题导致的服务不可达时,整个分布式锁服务是无法使用的。
为了解决单点问题,redis有主从哨兵机制,当主节点故障后,哨兵会做故障转移,从节点升级为主节点持续提供服务,但是由于主从复制是异步的,在下面场景下会出问题:
- 客户端A拿到了锁
- 主节点的信息还没有同步到从节点,主节点宕机了
- 哨兵把从节点升级为主节点
- 新的主节点锁是空闲的,没有被占用
- 客户端B请求锁也拿到了锁
- 出现了A、B两个客户端同时拿到锁的场景。
为了解决这个问题,redis引入了多节点分布式锁。
三、基于多个redis节点的分布式锁多节点分布式锁,在redis中叫Redlock(redis distribution lock)。其具体思想是引入N个redis节点,让客户端像这个N个节点以此请求加锁,如果客户端能够获得(N/2+1)
以上的锁,那么久可以认为客户端成功拿到乐锁,加锁成功,否则便认为加锁失败。这样即使有单个redis实例发生故障,以为锁变量在其他实例上也有保存,客户端任然可以获得锁。
具体步骤:
- 客户端获取当前时间
- 客户端依次按序像N个节点获取锁
- 要对加锁操作本身设置一个超时时间,加锁的超时时间应该远远小于业务锁的有效时间,一般几十毫秒即可。如果一个客户端超时,redis可以继续和下一个节点进行请求加锁。
- 完成和所有节点的加锁操作后,客户端计算加锁的耗时
- 判断是否加锁成功判断
- 获取到至少n/2+1的锁成功
- 加锁的耗时小于锁的有效时间
- 重新计算锁的有效时间,锁的最初有效时间减去加锁的耗时。如果新的有效时间不够完成共享数据的操作(就是锁内要做的事情),可以释放锁,以免操作没有完成锁却失效了,会出现多个客户端同时执行,失去了排他性。
- 如果第四步骤的判断是获取锁失败,那么需要执行取消锁操作,针对所有节点,包括那些加锁失败的节点(有些节点可能因为网络原因,实际加锁成功,但是返回客户端是失败)
单节点redis的redis分布式锁,相对比较简单,会出现偶尔的锁失效,如果允许这种场景,可以采用。
如果业务对并发的结果要求非常严格,建议使用redlock,但是整体部署维护成本较高。