zoukankan      html  css  js  c++  java
  • 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
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
  • 相关阅读:
    曾宪杰谈Java在淘宝的应用
    Spring 系列: Spring 框架简介
    Azul发布开源工具jHiccup,为Java提供运行时响应时间分析
    我在赶集网的两个月(完整版)
    汇编基础教程(一)——寄存器介绍(EFlags)
    PowerDesigner 12.5 开启注释列 将Comment(注释)及Name(名称)内容互相COPY的VBS代码 根据名称生成注释(完整示例)
    linux ubuntu SVN improt 项目 简单明了 一看就懂
    android WebView onJsAlert onJsConfirm
    汇编基础教程(二)——常用汇编指令之传送指令
    理解WebKit和Chromium: Chromium移动版(Chromium for Mobile: Android & iOS)
  • 原文地址:https://www.cnblogs.com/Brake/p/14380144.html
Copyright © 2011-2022 走看看