zoukankan      html  css  js  c++  java
  • redis(八)分布式锁

    概述

    文章将介绍分布式在redis中的运用,介绍了 RedLock 实现的思路还有功能的实现。

    分布式锁的动机

    当有多个 client 但是只有一个有执行权的分布式结构就可以考虑使用分布式锁。我们首先要知道锁的目的是为了达到执行的顺序性。

    RedLock 的设计

    两个必须达到的目标 : 安全和活性保障

    1. 安全属性 : 互斥,保证了同一个时刻只有一个 client 获得到锁。
    2. 活性属性A : 死锁释放,客户端crash 或是集群分区获取的锁可以释放
    3. 活性属性B : 错误容忍,只要大部分的 redis 节点都存活,客户端就有可能获取和释放锁。

    为什么只有 failover-base 的实现是不够的

    这是因为我们一般会使用 master-slave 实现主从复制达到高可用,然而有可能会出现以下情况 :

    1. Client A acquires the lock in the master.(clientA 在master 中获取到了锁)
    2. The master crashes before the write to the key is transmitted to the slave.(在master 复制写入到slave的时候,master down掉了)
    3. The slave gets promoted to master.(此时 slave 从原来masster继承,成为了新的master)
    4. Client B acquires the lock to the same resource A already holds a lock for. SAFETY VIOLATION!(由于旧的master down掉了,那么写入的请求没能复制成功,clientB在新的master中获取了锁,此时已有两个客户端拿到了锁)

    RedLock 的实现

    基于单Redis节点的分布式锁

    获取锁

    SET resource_name my_random_value NX PX 30000
    
    

    我们看到这里运用的主要有两个东西 :my_randow_value 和 过期时间。 my_random_value 对于每一个 client 都是唯一的,是用来顺序释放锁在一个安全的方式,下面用lua 来表示这个过程 : 假如这个锁存在,且当中的 my_random_value 正是和我的一样,那么就可以安全的删除。

    而关于过期时间

    当一个客户端获取锁成功之后,假如它崩溃了,或者由于发生了网络分割(network partition)导致它再也无法和Redis节点通信了,那么它就会一直持有这个锁,而其它客户端永远无法获得锁了

    if redis.call("get",KEYS[1]) == ARGV[1] then
        return redis.call("del",KEYS[1])
    else
        return 0
    end
    

    为什么要使用上这 my_random_value 呢?每一个client 的 my_random_value 都是相同的可以吗?考虑下面的情况。

    1. 客户端1获取锁成功。
    2. 客户端1在某个操作上阻塞了很长时间。
    3. 过期时间到了,锁自动释放了。
    4. 客户端2获取到了对应同一个资源的锁。
    5. 客户端1从阻塞中恢复过来,释放掉了客户端2持有的锁

    其实这种情况就像 CAS 中的 ABA 问题一样,后一个的操作并不知道资源被其他客户端持有了。

    ok,我们接下来看一下 RedLock 实现的分布式情况的分布式锁。

    RedLock 分布式锁过程简述

    以下描述来自参考资料 :

    它基于N个完全独立的Redis节点。

    1. 获取当前时间(毫秒数)。

    2. 按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过期时间(比如PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。

    3. 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。

    4. 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。

    5. 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的Redis Lua脚本)。

    可以看到纪录的时间没有一个统一的标准,假如存在某个节点时间流逝得比较快,

    实例 restart 的注意事项

    假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:

    1. 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。
    2. 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。
    3. 节点C重启后,客户端2锁住了C, D, E,获取锁成功。

    上面分析的由于节点重启引发的锁失效问题,总是有可能出现的。为了应对这一问题,antirez又提出了延迟重启(delayed restarts)的概念。也就是说,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。

    释放锁的注意事项

    在最后释放锁的时候,antirez在算法描述中特别强调,客户端应该向所有Redis节点发起释放锁的操作。也就是说,即使当时向某个节点获取锁没有成功,在释放锁的时候也不应该漏掉这个节点。这是为什么呢?设想这样一种情况,客户端发给某个Redis节点的获取锁的请求成功到达了该Redis节点,这个节点也成功执行了SET操作,但是它返回给客户端的响应包却丢失了。这在客户端看来,获取锁的请求由于超时而失败了,但在Redis这边看来,加锁已经成功了。因此,释放锁的时候,客户端也应该对当时获取锁失败的那些Redis节点同样发起请求。实际上,这种情况在异步通信模型中是有可能发生的:客户端向服务器通信是正常的,但反方向却是有问题的。

    Martin的分析

    Martin Kleppmann在2016-02-08这一天发表了一篇blog,名字叫”How to do distributed locking “,地址如下: https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html 他对redlock提出了几点质疑 :

    • RedLock有可能会由于GC导致锁失效
    • RedLock强依赖时间,本身的安全性是不够的。

    GC 对分布式锁的影响

    1297993-20200407095540783-1330354705.png

    可以看到当GC时执行的线程会阻塞从而导致锁过期,当客户端1从GC pause中恢复过来的时候,它不知道自己持有的锁已经过期了,它依然向共享资源(上图中是一个存储服务)发起了写数据请求,而这时锁实际上被客户端2持有,因此两个客户端的写请求就有可能冲突(锁的互斥作用失效了)。

    那既然GC是破坏锁互斥的重要因素,那不用GC环境可以了吗。M在文章也提出了电脑系统复杂,例如内存缺页等等都有可能导致这样的现象发生,M 提出了fencing token 的东西用于避免这类事件。

    1297993-20200407100150240-1179496212.png

    个人觉得很奇怪,这个fencing token 和 RedLock 中 my_random_value 的作用不是一样的吗,虽然 token 保持有顺序可是就是为了识别该资源已被其他客户端锁定。

    时间性强依赖导致的安全性

    来自参考资料的描述 :

    Martin在文中构造了一些事件序列,能够让Redlock失效(两个客户端同时持有锁)。为了说明Redlock对系统记时(timing)的过分依赖,他首先给出了下面的一个例子(还是假设有5个Redis节点A, B, C, D, E):

    • 客户端1从Redis节点A, B, C成功获取了锁(多数节点)。由于网络问题,与D和E通信失败。
    • 节点C上的时钟发生了向前跳跃,导致它上面维护的锁快速过期。
    • 客户端2从Redis节点C, D, E成功获取了同一个资源的锁(多数节点)。
    • 客户端1和客户端2现在都认为自己持有了锁。 上面这种情况之所以有可能发生,本质上是因为Redlock的安全性(safety property)对系统的时钟有比较强的依赖,一旦系统的时钟变得不准确,算法的安全性也就保证不了了。Martin在这里其实是要指出分布式算法研究中的一些基础性问题,或者说一些常识问题,即好的分布式算法应该基于异步模型(asynchronous model),算法的安全性不应该依赖于任何记时假设(timing assumption)。在异步模型中:进程可能pause任意长的时间,消息可能在网络中延迟任意长的时间,甚至丢失,系统时钟也可能以任意方式出错。一个好的分布式算法,这些因素不应该影响它的安全性(safety property),只可能影响到它的活性(liveness property),也就是说,即使在非常极端的情况下(比如系统时钟严重错误),算法顶多是不能在有限的时间内给出结果而已,而不应该给出错误的结果。这样的算法在现实中是存在的,像比较著名的Paxos,或Raft。但显然按这个标准的话,Redlock的安全性级别是达不到的。

    上面描述的这一段使我们想起了 CAP 中的 CP ,为了保持一致性,牺牲的只能是可用性。

    补充

    SETNX

    SETNX 命令的意思是 : [Set if Not exists] 也就说不存在的时候就会设置,SETNX 是不支持过期设置的,所以上文在实现通过Lua来达到执行的原子性。

    超时解锁导致并发

    当一个客户端获取锁后可能由于时间设置太短从而导致,未执行完锁就由于过期释放了,而其他客户端就可以加锁执行,从而会有两个客户端获得了资源。解决的方法如下 :

    • 增加执行时间作为过期时间,也即是增加过期时间
    • 增加守护线程,当快过期时增加过期时间

    锁可重入性

    我们知道java中,可以作为锁重入性判断的数据结构有 : ThreadLocal ,那么Redis是如何实现的呢?我们看一下 Redission 是如何实现的。

    // 如果 lock_key 不存在
    if (redis.call('exists', KEYS[1]) == 0)
    then
        // 设置 lock_key 线程标识 1 进行加锁
        redis.call('hset', KEYS[1], ARGV[2], 1);
        // 设置过期时间
        redis.call('pexpire', KEYS[1], ARGV[1]);
        return nil;
        end;
    // 如果 lock_key 存在且线程标识是当前欲加锁的线程标识
    if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)
        // 自增
        then redis.call('hincrby', KEYS[1], ARGV[2], 1);
        // 重置过期时间
        redis.call('pexpire', KEYS[1], ARGV[1]);
        return nil;
        end;
    // 如果加锁失败,返回锁剩余时间
    return redis.call('pttl', KEYS[1]);
    
    

    可以看到是使用一个 hset的结构来实现的,实际上 ThreadLocal 中也是用散列表来存放对应的数据的。

    释放锁的重试

    客户端当获取锁失败后会再次重试获取锁,那么重试功能的实现可以这样 :

    • 轮询
    • 信号通知,使用redis的发布订阅功能,当获取失败时,订阅锁释放的信息。 信号通知的过程如下,图来自 : https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/

    1297993-20200407104331124-1808925956.png

    总结

    RedLock 的实现需要依赖各节点的时间,这是我们需要关注的一点。文章讲了redLock实现思路和关于 RedLock 的一些争议,最后补充部分总结了几种功能实现的思路。

    参考资料

    • https://redis.io/topics/distlock (官方文档)
    • http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html (关于RedLock这个分布式锁的问题)
    • http://zhangtielei.com/posts/blog-redlock-reasoning.html (必看)
    • https://www.one-tab.com/page/Wuz27GojRK6uiiBMgKcbwQ (网页全集)
  • 相关阅读:
    pwnable.kr之input
    pwnable.kr之bof
    pwnable.kr之fd
    运维及服务器组成详解
    查看锁信息(开启InnoDB监控)
    【原创】记一次MySQL大表高并发写入引发CPU飙升的排障过程
    【原创】获取MySQL crash 时的core file
    【原创】MySQL Replay线上流量压测工具
    python面向对象
    TCP三次握手与四次挥手
  • 原文地址:https://www.cnblogs.com/Benjious/p/12651995.html
Copyright © 2011-2022 走看看