Redis实现分布式锁

一、分布式锁

在分布式系统中,当有多个客户端(跨进程,机器)需要获取锁时候,就需要分布式锁,这个锁保存在一个共享的存储系统中。

redis就是一个可以被多个客户端共享访问的存储系统,可以用来保存分布式锁,并且redis支持数万的并非操作,读写性能高,可以适应高并发的锁操作场景。

报文主要讨论两种类型的redis锁实现:

  1. 基于单个redis节点的分布式锁
  2. 基于多个redis节点的分布式锁(共享存储高可靠)、

二、基于单个redis节点的分布式锁

背景

  1. 锁变量名:lock_key。
  2. value=1:锁被持有。
  3. value=0:锁被释放。

2.1 示意图

加锁示意图

释放锁示意

2.2 redis命令支持

2.2.1 redis 命令支持-加锁-setnx

加锁需要三个操作

  1. 读取锁变量
  2. 判断锁变量的值
  3. 把锁变量设置为1

以上三个操作需要在一个原子操作里面完成,可以通过lua脚本来实现,但是redis提供了命令

setnx 可以用这个命令代替上面的三个操作,同时实现了原子操作。

setnx用于设置键值对的值,在执行时会判断键是否存在,如果不存在,就创建键,同时设置值,如果存在,就不做任何设置。

最终可以通过setnx的返回值来判断锁是否被持有:

  1. 返回值1,表示拿到锁
  2. 返回值0,标识锁失败

2.2.2 redis 命令支持-释放锁-del

del命令删除

2.2.3 setnx和del的组合的问题

可以通过setnx和del的组合实现加锁和释放锁的问题,但是setnx和del的组合有两个问题。

  1. setnx有锁永远无法释放的问题,考虑场景,客户端A使用setnx拿到锁以后,在执行自己业务逻辑的时候发送了异常,最终一直没有调用del来释放锁。锁一直被这个客户端A占用着,其他客户端无法拿到锁,着会严重影响业务.

    这种问题解决方案就是给setnx给一个默认的过期时间,当业务异常无法释放锁时,通过默认过期时间,redis会自动把这个锁置为无效,间接释放了锁。

  2. del操作最大的问题是因为锁对应的key是已知的,会出现key被误删的case,客户端A获取到了锁,在释放之前被客户端B给删除了,这个时候锁变成了空闲状态,客户端C又拿到了锁,最终的结果就是A和C同时拿到了锁。

    这个问题的核心就是只有加锁着本身才允许解除这个锁。可以在锁对应的值上做文章,让锁的值每次在加锁的时候都能唯一被识别(只有使用锁的客户端才能提供),比如加锁客户端所在的客户端专有信息等
    每次解锁的时候必须要传入这个唯一标识。

    1. 解锁前先获取锁对应的值
    2. 拿这个值和传入的唯一标识进行比较,如果一样才进行解锁,不一样,则不允许解锁
    3. 以上两个操作必须是一个原子操作

2.2.4 setnx和del的问题解决

分析了问题后就可以针对性解决。

  1. setnx-锁无法释放问题:redis提供了SET key value NX PX timeout。功能和setnx一样,但是会在timeout毫秒后自动过期,这就解决了客户端宕机后没有释放的问题。

  2. 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有主从哨兵机制,当主节点故障后,哨兵会做故障转移,从节点升级为主节点持续提供服务,但是由于主从复制是异步的,在下面场景下会出问题:

  1. 客户端A拿到了锁
  2. 主节点的信息还没有同步到从节点,主节点宕机了
  3. 哨兵把从节点升级为主节点
  4. 新的主节点锁是空闲的,没有被占用
  5. 客户端B请求锁也拿到了锁
  6. 出现了A、B两个客户端同时拿到锁的场景。

为了解决这个问题,redis引入了多节点分布式锁。

三、基于多个redis节点的分布式锁

多节点分布式锁,在redis中叫Redlock(redis distribution lock)。其具体思想是引入N个redis节点,让客户端像这个N个节点以此请求加锁,如果客户端能够获得(N/2+1)
以上的锁,那么久可以认为客户端成功拿到乐锁,加锁成功,否则便认为加锁失败。这样即使有单个redis实例发生故障,以为锁变量在其他实例上也有保存,客户端任然可以获得锁。

具体步骤:

  1. 客户端获取当前时间
  2. 客户端依次按序像N个节点获取锁
    1. 要对加锁操作本身设置一个超时时间,加锁的超时时间应该远远小于业务锁的有效时间,一般几十毫秒即可。如果一个客户端超时,redis可以继续和下一个节点进行请求加锁。
  3. 完成和所有节点的加锁操作后,客户端计算加锁的耗时
  4. 判断是否加锁成功判断
    1. 获取到至少n/2+1的锁成功
    2. 加锁的耗时小于锁的有效时间
  5. 重新计算锁的有效时间,锁的最初有效时间减去加锁的耗时。如果新的有效时间不够完成共享数据的操作(就是锁内要做的事情),可以释放锁,以免操作没有完成锁却失效了,会出现多个客户端同时执行,失去了排他性。
  6. 如果第四步骤的判断是获取锁失败,那么需要执行取消锁操作,针对所有节点,包括那些加锁失败的节点(有些节点可能因为网络原因,实际加锁成功,但是返回客户端是失败)

四、选择

单节点redis的redis分布式锁,相对比较简单,会出现偶尔的锁失效,如果允许这种场景,可以采用。

如果业务对并发的结果要求非常严格,建议使用redlock,但是整体部署维护成本较高。

作者:iBrake
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
原文地址:https://www.cnblogs.com/Brake/p/14380144.html