分布式锁进化史

目录

1、基于setnx + expire

2、基于getset

3、基于setnx

4、基于setnx + lua脚本

5、基于set

6、分布式锁RedLock


1、基于setnx + expire

//以下是伪代码
lock(){
    setnx key;//1
    expire key seconds;//2
}
unlock(){
    del key;
}

//1 使用setnx命令,当key存在的时候返回null,当key不存在的时候设置key

//2 给key设置一个过期时间,这是为了避免应用在服务重启或者出现异常时导致锁无法释放的情况

问题:

    如果执行完//1处的代码后,服务重启或者出现异常,那么//2就不会执行,也就是说锁没有过期时间。这个问题可以使用lua脚本(原子性)解决,将setnx命令和expire命令放入lua脚本中一起执行,但是如果redis server在执行lua脚本中的setnx命令后crash,依然会导致expire不再执行,然后锁没有过期时间,最终无法释放。

2、基于getset

//以下是伪代码
lock(){
    newExpireTime = currentTime + expireSeconds;
    if(setnx key newExpireTime seconds){//1
        oldExpireTime = get(key);//2
        if(oldExpireTime < currentTime){
            newExpireTime = currentTime + expireSeconds;
            currentExpireTime = getset(key,newExpireTime);//3
            if(currentExpireTime == oldExpireTime){//4
                return 1;
            }else{
                return 0;
            }
        }
    }
}
unlock(){
    del key;
}

//1 使用setnx获取锁

//2 如果获取锁失败,通过get命令返回的时间戳检查锁是否已经过期

//3 getset命令修改value为newExpireTime

//4 检查getset返回的旧值,如果等于get返回的值,则认为获取锁成功

问题:

    在锁竞争比较激烈的时候,会出现value不断被修改,但是没有一个client成功获取锁;

    在获取锁的过程中不断的修改原有锁的数据,设想一种场景C1,C2竞争锁,C1获取到了锁,C2锁执行了GETSET操作修改了C1锁的过期时间,如果C1没有正确释放锁,锁的过期时间被延长,其它Client需要等待更久的时间。

3、基于setnx

//以下是伪代码
lock(){
    setnx key 1 seconds
}
unlock(){
    del key
}

redis 2.6.12之后setnx增加了过期时间参数,这样可以解决原先的setnx和expire两条命令无法保证原子性的问题

问题:

    存在以下情况:

        1、C1成功获取了锁L1,然后C1因为某些原因导致程序执行时间过长,导致在锁L1失效前C1都没有主动释放锁

        2、C2在C1的锁L1失效后获取到了锁L1,并且开始执行程序,此时C1还没有执行完,所以可能会出现C1和C2同时执行而导致的数据不一致等问题

        3、C1如果早于C2执行完毕,那么C1就会释放已经被C2获取的锁L1。此时,如果C3尝试获取锁L1的话,就会成功

4、基于setnx + lua脚本

//以下是伪代码
lock(){
    setnx key 1 seconds
}
unlock(){
    eval(
        //lua脚本
        if redis.call("get",KEYS[1]) == ARGV[1] then
            return redis.call("del",KEYS[1])
        else
            return 0
        end
    )
}

通过指定Value为时间戳,并在释放锁的时候检查锁的Value是否为获取锁的Value。规避了C1释放了C2持有的锁的问题。

在释放锁的时候因为涉及到多个Redis操作,并且考虑到Check And Set 模型的并发问题,所以使用Lua脚本来避免并发问题。

 

其实还有一种方式来解决C1释放了C2持有的锁的问题,就是在C1获取锁的时候,开启一个后台线程,每隔一段时间检查一下锁,如果锁还是被C1持有,则延长锁的expireTime。具体实现可以去看Redisson的代码。

 

问题:

    如果在并发极高的场景下,比如抢红包场景,可能存在UnixTimestamp重复问题,另外由于不能保证分布式环境下的物理时钟一致性,也可能存在UnixTimestamp重复问题,只不过极少情况下会遇到。

5、基于set

//以下是伪代码
lock(){
    set key uniqId seconds
}
unlock(){
    eval(
        //lua脚本
        if redis.call("get",KEYS[1]) == ARGC[1] then
            return redis.call("del",EKYS[1])
        else
            return 0
        end
    )
}

redis 2.6.12后set同样提供了一个nx参数,等同于setnx命令,官方文档上提醒后面的版本有可能去掉setnx,setex,psetex,并用set命令替代,另外一个优化是使用一个自增的唯一uniqId代替时间戳来规避4中提到的时钟问题

问题:

    在redis集群中会遇到如下问题:

        由于redis集群数据同步为异步,假设在master节点获取到锁后未完成数据同步时候,master节点crash,然后slave节点升级为master节点,那么新的master节点依然可以获取锁,也就是说多个client同时获取到了锁

6、分布式锁RedLock

针对如何实现分布式Redis的锁,国外的分布式专家有过激烈的讨论, antirez提出了分布式锁算法Redlock,在distlock话题下可以看到对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脚本)。

    6、释放锁:对所有的Redis节点发起释放锁操作

然而Martin Kleppmann针对这个算法提出了质疑,提出应该基于fencing token机制(每次对资源进行操作都需要进行token验证)

    1、Redlock在系统模型上尤其是在分布式时钟一致性问题上提出了假设,实际场景下存在时钟不一致和时钟跳跃问题,而Redlock恰恰是基于timing的分布式锁

    2、另外Redlock由于是基于自动过期机制,依然没有解决长时间的gc pause等问题带来的锁自动失效,从而带来的安全性问题。

接着antirez又回复了Martin Kleppmann的质疑,给出了过期机制的合理性,以及实际场景中如果出现停顿问题导致多个Client同时访问资源的情况下如何处理。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值