zoukankan      html  css  js  c++  java
  • redis并发访问的问题之实现分布式锁

    在分布式系统中,当有多个客户端需要获取锁时,就需要分布式锁,此时,锁时保存在一个共享存储系统中等,可以被多个客户端共享访问和获取

    Redis 本身可以被多个客户端共享访问,正好就是一个共享存储系统,可以用来保存分布式锁。

    在介绍分布式锁之前要先介绍一下单机上的锁

    单机上的锁:
    对于在单机上运行的多线程程序来说,锁本身可以用一个变量表示。变量值为 0 时,表示没有线程获取锁;变量值为 1 时,表示已经有线程获取到锁了。实际上一个线程加锁操作就是检查锁变量的值是否为 0。如果是 0,

    就把锁的变量值设置为 1,表示获取到锁,如果不是 0,就返回错误信息,表示加锁失败,已经有别的线程获取到锁了。而一个线程调用释放锁操作,其实就是将锁变量的值置为 0,以便其它线程可以来获取锁。

    分布式锁同样可以用一个变量来实现。客户端加锁和释放锁的操作逻辑,也和单机上的加锁和释放锁操作逻辑一致:加锁时同样需要判断锁变量的值,根据锁变量值来判断能否加锁成功;释放锁时需要把锁变量值设置为 0,表明客户端不再持有锁。只不过这个变量需要多个redis实例共同维护。

    在分布式场景下,锁变量需要由一个共享存储系统来维护,只有这样,多个客户端才可以通过访问共享存储系统来访问锁变量。相应的,加锁和释放锁的操作就变成了读取、判断和设置共享存储系统中的锁变量值。

    对分布式锁的两个要求:

    分布式锁的加锁和释放锁的过程,涉及多个操作。所以,在实现分布式锁时,我们需要保证这些锁操作的原子性(因为释放锁的过程涉及到 读取锁,修改变量,删除锁,所以需要保证操作的原子性);

    要求二:共享存储系统保存了锁变量,如果共享存储系统发生故障或宕机,那么客户端也就无法进行锁操作了。在实现分布式锁时,我们需要考虑保证共享存储系统的可靠性,进而保证锁的可靠性。

    基于单个redis节点实现分布式锁:
    作为分布式锁实现过程中的共享存储系统,Redis 可以使用键值对来保存锁变量,再接收和处理不同客户端发送的加锁和释放锁的操作请求。Redis 可以使用一个键值对 lock_key:0 来保存锁变量,其中,键是 lock_key,也是锁变量的名称,锁变量的初始值是 0。如果一个客户端申请加锁,就将其值设为1,释放锁时在将其值置为

    加锁包含了三个操作(读取锁变量、判断锁变量值以及把锁变量值设置为 1),而这三个操作在执行时需要保证原子性。--这三个操作都需要原子性,否则的话在第一步,如果两个线程并发读取到锁变量,然后同时进行修改,那么锁变量的值就会错误,会有两个线程访问到临界区代码。

    想要保证原子性的方式有来两种,单命令操作和使用lua脚本

    但命令操作:setnx 如果键不存在就设置,存在的话就不做任何设置
    // 加锁
    SETNX lock_key 1
    // 业务逻辑
    DO THINGS
    // 释放锁
    DEL lock_key

    但是这样会出现问题,

    1:客户端加锁后,执行完业务逻辑后,因为网络原因没有执行del,造成锁一直被一个客户端所有,这样的话,锁就无法被其他客户端获得

    解决方法:
    给锁变量设置一个过期时间。这样一来,即使持有锁的客户端发生了异常,无法主动地释放锁,Redis 也会根据锁变量的过期时间,在锁变量过期后,把它删除。

    其它客户端在锁变量过期后,就可以重新请求加锁,这就不会出现无法加锁的问题了。

    2:一个客户端执行加锁后,而另一个客户端执行del命令,造成临界资源暴露

    解决方案:

    在加锁操作时,可以让每个客户端给锁变量设置一个唯一值,这里的唯一值就可以用来标识当前操作的客户端。在释放锁操作时,客户端需要判断,当前锁变量的值是否和自己的唯一标识相等,只有在相等的情况下,才能释放锁。这样一来,就不会出现误释放锁的问题了。

    Redis 给 SET 命令提供了类似的选项 NX,用来实现“不存在即设置”。如果使用了 NX 选项,SET 命令只有在键值对不存在时,才会进行设置,否则不做赋值操作。此外,

    SET 命令在执行时还可以带上 EX 或 PX 选项,用来设置键值对的过期时间。

    key 的存活时间由 seconds 或者 milliseconds 选项值来决定。
    SET key value [EX seconds | PX milliseconds]  [NX]
    // 加锁, unique_value作为客户端唯一性的标识
    SET lock_key unique_value NX PX 10000

    释放锁过程

    //释放锁 比较unique_value是否相等,避免误释放

    if redis.call("get",KEYS[1]) == ARGV[1] then   

    return redis.call("del",KEYS[1])

    else    return 0

    end

    在释放锁操作中,使用了 Lua 脚本,这是因为,释放锁操作的逻辑也包含了读取锁变量、判断值、删除锁变量的多个操作,而 Redis 在执行 Lua 脚本时,

    可以以原子性的方式执行,从而保证了锁释放操作的原子性。

    基于多个redis实例实现分布式锁方式:

    只用了一个 Redis 实例来保存锁变量,如果这个 Redis 实例发生故障宕机了,那么锁变量就没有了。此时,客户端也无法进行锁操作了,这就会影响到业务的正常执行。所以,我们在实现分布式锁时,还需要保证锁的可靠性。这就要提到基于多个 Redis 节点实现分布式锁的方式了。

    如果用多实例实现分布锁就要用到分布式锁算法 Redlock。

    Redlock算法基本思想是让客户端依次向所有实例请求加锁,如果客户端能够和半数以上的实例成功完成加锁操作,那么我们就认为客户端成功获得分布式锁,否则加锁失败。这样一来,即使有单个 Redis 实例发生故障,因为锁变量在其它实例上也有保存,所以,客户端仍然可以正常地进行锁操作,锁变量并不会丢失。因为向多个redis实例申请加锁,必须要规定一个加锁超时时间,否则如果一直阻塞在加锁步骤时,算法就会失效

    Redlock 算法的执行步骤:

    第一步是,客户端获取当前时间。

    第二步是,客户端按顺序依次向 N 个 Redis 实例执行加锁操作。

    这里的加锁操作和在单实例上执行的加锁操作一样,使用 SET 命令,带上 NX,EX/PX 选项,以及带上客户端的唯一标识。当然,如果某个 Redis 实例发生故障了,为了保证在这种情况下,Redlock 算法能够继续运行,我们需要给加锁操作设置一个超时时间。加锁操作的超时时间需要远远地小于锁的有效时间,一般也就是设置为几十毫秒。

    第三步是,一旦客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过程的总耗时。

    客户端只有在满足下面的这两个条件时,才能认为是加锁成功。

    条件一:客户端从超过半数(大于等于 N/2+1)的 Redis 实例上成功获取到了锁;

    条件二:客户端获取锁的总耗时没有超过锁的有效时间。在满足了这两个条件后,我们需要重新计算这把锁

    在满足了这两个条件后,我们需要重新计算这把锁的有效时间,计算的结果是锁的最初有效时间减去客户端为获取锁的总耗时。如果锁的有效时间已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。

     相当于如果第一个实例加锁成功,且最后投票成功的话,那么锁的有效时间为第一个申请加锁的有效时间-加锁总耗时(执行redlock算法时间)当然,如果客户端在和所有实例执行完加锁操作后,没能同时满足这两个条件,那么,客户端向所有 Redis 节点发起释放锁的操作。

    总结:分布式锁得注意事项:
    1、使用 SET $lock_key $unique_val EX $second NX 命令保证加锁原子性,并为锁设置过期时间

    2、锁的过期时间要提前评估好,要大于操作共享资源的时间

    3、每个线程加锁时设置随机值,释放锁时判断是否和加锁设置的值一致,防止自己的锁被别人释放

    4、释放锁时使用 Lua 脚本,保证操作的原子性

    5、基于多个节点的 Redlock,加锁时超过半数节点操作成功,并且获取锁的耗时没有超过锁的有效时间才算加锁成功

    6、Redlock 释放锁时,要对所有节点释放(即使某个节点加锁失败了),因为加锁时可能发生服务端加锁成功,由于网络问题,给客户端回复网络包失败的情况,所以需要把所有节点可能存的锁都释放掉

    7、使用 Redlock 时要避免机器时钟发生跳跃,需要运维来保证,对运维有一定要求,否则可能会导致 Redlock 失效。例如共 3 个节点,线程 A 操作 M,N 节点加锁成功,但其中 N个节点机器时钟发生跳跃,锁提前过期,线程 B 正好在另外 N,O节点也加锁成功,此时 Redlock 相当于失效了(Redis 作者和分布式系统专家争论的重要点就在这)相当于一个线程误判成功了,一个线程成功了造成两个线程访问了临界资源。

    8、如果为了效率,使用基于单个 Redis 节点的分布式锁即可,此方案缺点是允许锁偶尔失效,优点是简单效率高

    9、如果是为了正确性,业务对于结果要求非常严格,建议使用 Redlock,但缺点是使用比较重,部署成本高

  • 相关阅读:
    Linux-文件目录管理
    20. 有效的括号
    242. 有效的字母异位词
    387. 字符串中的第一个唯一字符
    136. 只出现一次的数字
    14. 最长公共前缀
    268. 丢失的数字
    169. 多数元素
    26. 删除有序数组中的重复项
    283. 移动零
  • 原文地址:https://www.cnblogs.com/foreverlearnxzw/p/13892497.html
Copyright © 2011-2022 走看看