美文网首页技术架构分布式
面试不懂分布式锁?看这篇文章就够了

面试不懂分布式锁?看这篇文章就够了

作者: PENG先森_晓宇 | 来源:发表于2019-07-25 16:55 被阅读0次

    背景

    分布式锁,是指在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问。

    分布式锁要满足哪些要求呢?

    • 排他性:在同一时间只会有一个客户端能获取到锁,其它客户端无法同时获取

    • 避免死锁:这把锁在一段有限的时间之后,一定会被释放(正常释放或异常释放)

    • 高可用:获取或释放锁的机制必须高可用且性能佳

    分布式锁的实现方式

    目前主流的有三种,从实现的复杂度上来看,从上往下难度依次增加:

    • 基于数据库实现

    • 基于Redis实现

    • 基于ZooKeeper实现

    无论哪种方式,其实都不完美,依旧要根据咱们业务的实际场景来选择。

    为什么用分布式锁?

    在讨论这个问题之前,我们先来看一个业务场景:

    系统A是一个电商系统,目前是一台机器部署,系统中有一个用户下订单的接口,该接口的流程是用户下订单之前一定要去检查一下库存,确保库存足够了才会给用户下单

    由于系统有一定的并发,所以会预先将商品的库存保存在redis中,用户下单的时候会更新redis的库存。

    此时系统架构如下:

    image

    但是这样一来会产生一个问题:假如某个时刻,redis里面的某个商品库存为1,此时两个请求同时到来,其中一个请求执行到上图的第3步,更新数据库的库存为0,但是第4步还没有执行。

    而另外一个请求执行到了第2步,发现库存还是1,就继续执行第3步。

    这样的结果,是导致卖出了2个商品,然而其实库存只有1个。

    很明显不对啊!这就是典型的\color{green}{库存超卖}问题,产生这种问题实质是2 3 4步骤不是原子操作

    此时,我们很容易想到解决方案:用锁把2、3、4步锁住(其实是把2步骤锁住),让他们执行完4步骤之后把锁解开,另一个线程才能进来执行第2步。这里又会涉及到另一个问题,数据库和缓存一致性的问题,即3 4步骤,这里不做讨论。

    image

    按照上面的图,在执行第2步时,使用Java提供的synchronized或者ReentrantLock来锁住,然后在第4步执行完之后才释放锁。

    这样一来,2、3、4 这3个步骤就被“锁”住了,多个线程之间只能串行化执行。

    但是好景不长,整个系统的并发飙升,一台机器扛不住了。现在要增加一台机器,如下图:

    image

    增加机器之后,系统变成上图所示,我的天!

    假设此时两个用户的请求同时到来,但是落在了不同的机器上,那么这两个请求是可以同时执行了,还是会出现库存超卖的问题。

    为什么呢?因为上图中的两个A系统,运行在两个不同的JVM里面,他们加的锁只对属于自己JVM里面的线程有效,对于其他JVM的线程是无效的。

    因此,这里的问题是:Java提供的原生锁机制在多机部署场景下失效了

    这是因为两台机器加的锁不是同一个锁(两个锁在不同的JVM里面)。

    那么,我们只要保证两台机器加的锁是同一个锁,问题不就解决了吗?

    此时,就该分布式锁隆重登场了,分布式锁的思路是:

    在整个系统提供一个全局、唯一的获取锁的“东西”,然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。

    至于这个“东西”,可以是Redis、Zookeeper,也可以是数据库。

    文字描述不太直观,我们来看下图:

    image

    通过上面的分析,我们知道了库存超卖场景在分布式部署系统的情况下使用Java原生的锁机制无法保证线程安全,所以我们需要用到分布式锁的方案。

    那么,如何实现分布式锁呢?接着往下看!

    基于数据库实现分布式锁(悲观锁)

    拿换领码换领优惠券按理来说

    1. 查该码状态是否已经被换取,如果未换取执行下一步
    2. 换取优惠券
    3. 更新该码状态

    看似逻辑很正常,但如果用户同一时间并发提交换领码,在没有加锁限制的情况下,用户则可以使用同一个换领码同时兑换到多张优惠券。比如用户A在执行完第二步但还没执行第三步时,用户B也使用该码进行兑换,在第一步是查询到的是该码未被兑换。

    所以在第一步的时候就要加锁。为代码如下

    start transaction;
    //加排他锁,有别的线程并发读的时候,此处就处于加锁状态(阻塞状态)
    select state from coupon where number='11111143' for update;
    ....
    换取优惠券
    ....
    //更新状态码状态
    update coupon set state=1 where number='1111143';
    

    这样在第一步查询时就加锁,这样就可以防止同一码被多次兑换。如果使用正常的select state from coupon where number='11111143',在mvcc中读是不加锁的。如果使用共享锁select state from coupon where number='11111143' lock in share mode的话,并发时其他线程也是可以获得该共享锁的。所以这里只能用排他锁。

    总结:如果第一步查询时是使用redis查询的话,就使用redis来实现分布式锁,如果是数据库查询时就可以使用数据库锁来实现。

    基于Redis实现分布式锁

    上面分析为啥要使用分布式锁了,这里我们来具体看看分布式锁落地的时候应该怎么样处理。

    最常见的一种方案就是使用Redis做分布式锁

    使用Redis做分布式锁的思路大概是这样的:在redis中设置一个值表示加了锁,然后释放锁的时候就把这个key删除。

    具体代码是这样的:


    这种方式有几大要点:

    • 一定要用使用SET key value NX PX milliseconds 命令(原子操作)

      如果不用这种方式,采用先设置了值,再设置过期时间,这个不是原子性操作,有可能在设置过期时间之前宕机,会造成死锁(key永久存在)。

      这种方式一定要设置过期时间,同样也是避免死锁。如果没设置过期时间的情况下,机器(不是redis服务器,指api服务器)获取锁成功,但是删除锁时机器宕机,这是造成死锁,其他线程将一直获取不到该锁。

    • value要具有唯一性

      这个是为了在解锁的时候,需要验证value是和加锁的一致才删除key。

      这是避免了一种情况:假设A获取了锁,过期时间30s,此时35s之后,锁已经自动释放了,A去释放锁,但是此时可能B获取了锁。A客户端就不能删除B的锁了。

      如果真出现A超过过期时间还没有执行完的情况,这里也会出现一个很严重的问题,也就是线程安全问题,同样会出现上述线程超卖的情况。所以要使用这种方式的话,过期时间一定要合理控制。

    image

    除了要考虑客户端要怎么实现分布式锁之外,还需要考虑redis的部署问题。

    redis有3种部署方式:

    • 单机模式

    • master-slave + sentinel选举模式

    • redis cluster模式

    使用redis做分布式锁的缺点在于:如果采用单机部署模式,会存在单点问题,只要redis故障了。加锁就不行了。

    采用master-slave模式,加锁的时候只对一个节点加锁,即便通过sentinel做了高可用,但是如果master节点故障了,发生主从切换,此时就会有可能出现锁丢失的问题。

    如果采用redis cluster模式,同样会有上述问题,加锁会先对集群中的某个主节点加锁,然后异步复制到其备节点。如果还没复制到备节点时,主节点宕机,这时备节点上选举为主节点。这把锁已经丢失。

    所以这种方式有以下俩个缺点:

    • 会有锁丢失的问题
    • set key value nx px 3000 如果3000毫秒之后业务逻辑还没执行完,key过期,锁释放。其他线程获取到锁。这样一来也会造成线程安全问题。

    php代码实现如下:模拟下单间库存,避免库存超卖现象

    public function lock(Request $request, Response $response)
        {
            $data = $this->validate($request, [
                'shop_id' => 'required',
                'num' => 'required'
            ]);
            $lockState = false;
            $lockValue = microtime(true);
            while (!$lockState) {
                //加锁 set aaa 2324242 px 60000 nx
                $lockState = Redis::set($data['shop_id'] . "lock", $lockValue, 'px', 60000, 'nx');
                //避免重复无用循环,浪费cpu等资源
                sleep(1)
            }
            //获取库存缓存,要预热,或者后台添加商品时添加缓存
            $inventory = Redis::get($data['shop_id'] . "sku");
            if ($inventory <= 0) {
                //库存为0 已经卖完
                return $response->setStatusCode(401);
            }
            // 下单
    
    
            //修改库存,涉及到双写一致问题
            try {
                //加事务
                DB::beginTransaction();
                //修改数据库库存
                $updateInventory = Shop::where('id', $data['shop_id'])->decrement('inventory', $data['num']);
                if ($updateInventory) {
                    //库存存入
                    Redis::decrby($data['shop_id'] . "sku", $data['num']);
                }
                DB::commit();
            } catch (\Exception $e) {
                $this->addError('sku error', ['error' => $e->getMessage()]);
                DB::rollBack();
            }
            //解锁 用到了get和delete操作 使用lua脚本
            Redis::eval(file_get_contents(storage_path("app/lock.lua")), 1, $data['shop_id'] . "lock", $lockValue);
    
        }
    

    lua脚本如下

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

    要注意:获取锁是一个阻塞的过程,如果锁被线程A占用,这时线程B来获取时会一直处于阻塞状态,直到线程A释放锁时线程B才会获取锁。

    使用这种方法需要注意几个问题:

    1. 首先第一个问题,这个锁必须要设置一个过期时间。否则的话,当一个客户端获取锁成功之后,假如它崩溃了,或者由于发生了网络分割(network partition)导致它再也无法和Redis节点通信了,那么它就会一直持有这个锁,而其它客户端永远无法获得锁了。antirez在后面的分析中也特别强调了这一点,而且把这个过期时间称为锁的有效时间(lock validity time)。获得锁的客户端必须在这个时间之内完成对共享资源的访问。

    2. 第二个问题,第一步获取锁的操作,网上不少文章把它实现成了两个Redis命令:

    SETNX resource_name my_random_value
    EXPIRE resource_name 30
    

    虽然这两个命令和前面算法描述中的一个SET命令执行效果相同,但却不是原子的。如果客户端在执行完SETNX后崩溃了,那么就没有机会执行EXPIRE了,导致它一直持有这个锁。

    1. 第三个问题,也是antirez指出的,设置一个随机字符串my_random_value是很有必要的,它保证了一个客户端释放的锁必须是自己持有的那个锁。假如获取锁时SET的不是一个随机字符串,而是一个固定值,那么可能会发生下面的执行序列:
    • 客户端1获取锁成功。

    • 客户端1在某个操作上阻塞了很长时间。

    • 过期时间到了,锁自动释放了。

    • 客户端2获取到了对应同一个资源的锁。

    • 客户端1从阻塞中恢复过来,释放掉了客户端2持有的锁。

    之后,客户端2在访问共享资源的时候,就没有锁为它提供保护了。

    1. 第四个问题,释放锁的操作必须使用Lua脚本来实现。释放锁其实包含三步操作:'GET'、判断和'DEL',用Lua脚本来实现能保证这三步的原子性。否则,如果把这三步操作放到客户端逻辑中去执行的话,就有可能发生与前面第三个问题类似的执行序列:
    • 客户端1获取锁成功。
    • 客户端1访问共享资源。

    • 客户端1为了释放锁,先执行'GET'操作获取随机字符串的值。

    • 客户端1判断随机字符串的值,与预期的值相等。

    • 客户端1由于某个原因阻塞住了很长时间。

    • 过期时间到了,锁自动释放了。

    • 客户端2获取到了对应同一个资源的锁。

    *客户端1从阻塞中恢复过来,执行DEL操纵,释放掉了客户端2持有的锁。

    实际上,在上述第三个问题和第四个问题的分析中,如果不是客户端阻塞住了,而是出现了大的网络延迟,也有可能导致类似的执行序列发生。



    基于以上的考虑,其实redis的作者也考虑到这个问题,他提出了一个RedLock算法

    假设redis的部署模式是redis cluster,总共有5个master节点,通过以下步骤获取一把锁:

    • 获取当前时间戳,单位是毫秒

    • 它尝试按顺序获取所有5个实例中的锁,在所有实例中使用相同的键名和随机值,并设置一个锁的过期时间。在步骤2期间,客户端使用与锁的过期时间相比较小的时间以获取锁,这个时间叫做的锁的获取时间。例如,如果过期时间是10秒,则锁的获取时间可以在~5-50毫秒范围内(如果超时则获取失败,从而尝试获取下个锁)。这可以防止客户端长时间保持阻塞,试图与Redis节点进行通信,如果实例不可用,我们应该尝试尽快与下一个实例通话。

    • 客户端通过从当前时间中减去在步骤1中获得的时间戳来计算锁的获取时间。当且仅当客户端能够在大多数实例中获取锁时(至少3个)并且获取锁时间小于锁定锁的过期时间,认为锁定被获取。

    • 如果获得了锁,则其有效时间被认为是锁的过期时间减去获取锁时间,如步骤3中计算的。

    • 如果客户端由于某种原因(无法锁定5 / 2 + 1实例或有效时间为负)未能获取锁定,它将尝试解锁所有实例(即使它认为不是能够锁定)。

    • 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁

    当然,上面描述的只是获取锁的过程,而释放锁的过程比较简单:客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否。
    具体步骤可参考redis 官网

    但是这样的这种算法还是颇具争议的,可能还会存在不少的问题,无法保证加锁的过程一定正确。

    image

    redLock锁真的万无一失吗?下面是需要思考的几个问题

    • 未解决执行时间大于过期时间问题
      单节点时存在如果线程A的执行时间或者发生阻塞以至于大于锁的加锁时间,这时线程A还没有执行完,由于锁已经自动释放,这时线程B获得锁。同一资源被俩线程执行,出现安全问题。redLock同样未解决该问题,所以锁的过期时间时一个很关键的问题。

    • RedLock性能及崩溃恢复的相关解决方法
      假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:

      1. 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。

      2. 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。

      3. 节点C重启后,客户端2锁住了C, D, E,获取锁成功。

      这样,客户端1和客户端2同时获得了锁(针对同一资源)。在默认情况下,Redis的AOF持久化方式是每秒写一次磁盘(即执行fsync),因此最坏情况下可能丢失1秒的数据。为了尽可能不丢数据,Redis允许设置成每次修改数据都进行fsync,但这会降低性能。当然,即使执行了fsync也仍然有可能丢失数据(这取决于系统而不是Redis的实现)。所以,上面分析的由于节点重启引发的锁失效问题,总是有可能出现的。为了应对这一问题,antirez又提出了延迟重启(delayed restarts)的概念。也就是说,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。

    • 基于故障转移实现的redis主从无法真正实现Redlock

      假设redis cluster同样有5个master节点,A, B, C, D, E。设想发生了如下的事件序列:

      1. 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。

      2. 节点C崩溃,并且崩溃前锁未同步到slave节点锁,slave节点升级为master节点,这时该master节点并没有加锁。

      3. 这时客户端2锁住了C, D, E,获取锁成功。

      这样,客户端1和客户端2同时获得了锁(针对同一资源),导致资源共享,没有进行互斥。这个失败的原因是因为从redis立刻升级为主redis,如果能够过TTL时间再升级为主redis(延迟升级)后,或者立刻升级为主redis的节点只有过TTL的时间后再执行获取锁的任务,就能成功产生互斥效果;是不是这样就能实现基于redis主从的Redlock了

    RedLock算法是否是异步算法?

    可以看成是同步算法;因为 即使进程间(多个电脑间)没有同步时钟,但是每个进程时间流速大致相同;并且时钟漂移相对于TTL叫小,可以忽略,所以可以看成同步算法;(不够严谨,算法上要算上时钟漂移,因为如果两个电脑在地球两端,则时钟漂移非常大)

    RedLock失败重试

    当client不能获取锁时,应该在随机时间后重试获取锁;并且最好在同一时刻并发的把set命令发送给所有redis实例;而且对于已经获取锁的client在完成任务后要及时释放锁,这是为了节省时间;

    RedLock释放锁

    由于释放锁时会判断这个锁的value是不是自己设置的,如果是才删除;所以在释放锁时非常简单,只要向所有实例都发出释放锁的命令,不用考虑能否成功释放锁;

    RedLock注意点(Safety arguments):

    1.先假设client获取所有实例,所有实例包含相同的key和过期时间(TTL) ,但每个实例set命令时间不同导致不能同时过期,第一个set命令之前是T1,最后一个set命令后为T2,则此client有效获取锁的最小时间为TTL-(T2-T1)-时钟漂移;

    2.对于以N/2+ 1(也就是一半以 上)的方式判断获取锁成功,是因为如果小于一半判断为成功的话,有可能出现多个client都成功获取锁的情况, 从而使锁失效

    3.一个client锁定大多数事例耗费的时间大于或接近锁的过期时间,就认为锁无效,并且解锁这个redis实例(不执行业务) ;只要在TTL时间内成功获取一半以上的锁便是有效锁;否则无效

    总结

    1.TTL时长 要大于正常业务执行的时间+获取所有redis服务消耗时间+时钟漂移

    2.获取redis所有服务消耗时间要 远小于TTL时间,并且获取成功的锁个数要 在总数的一般以上:N/2+1

    3.尝试获取每个redis实例锁时的时间要 远小于TTL时间

    4.尝试获取所有锁失败后 重新尝试一定要有一定次数限制

    5.在redis崩溃后(无论一个还是所有),要延迟TTL时间重启redis

    6.在实现多redis节点时要结合单节点分布式锁算法 共同实现

    最后,Martin得出了如下的结论:

    • 如果是为了效率(efficiency)而使用分布式锁,允许锁的偶尔失效,那么使用单Redis节点的锁方案就足够了,简单而且效率高。Redlock则是个过重的实现(heavyweight)。

    • 如果是为了正确性(correctness)在很严肃的场合使用分布式锁,那么不要使用Redlock。它不是建立在异步模型上的一个足够强的算法,它对于系统模型的假设中包含很多危险的成分(对于timing)。而且,它没有一个机制能够提供fencing token。那应该使用什么技术呢?Martin认为,应该考虑类似Zookeeper的方案,或者支持事务的数据库。

    另一种方式:Redisson

    此外,实现Redis的分布式锁,除了自己基于redis client原生api来实现之外,还可以使用开源框架:Redission

    Redisson是一个企业级的开源Redis Client,也提供了分布式锁的支持。我也非常推荐大家使用,为什么呢?

    回想一下上面说的,如果自己写代码来通过redis设置一个值,是通过下面这个命令设置的。

    • SET anyLock unique_value NX PX 30000

    这里设置的超时时间是30s,假如我超过30s都还没有完成业务逻辑的情况下,key会过期,其他线程有可能会获取到锁。

    这样一来的话,第一个线程还没执行完业务逻辑,第二个线程进来了也会出现线程安全问题。所以我们还需要额外的去维护这个过期时间,太麻烦了~

    我们来看看redisson是怎么实现的?先感受一下使用redission的爽:

            Config config = new Config();
            config . useClusterServers()
                . addNodeAddress("redis://192.168.31.101:7001")
                . addNodeAddress("redis://192.168.31.101:7002")
                . addNodeAddress("redis://192.168.31.101:7003")
                . addNodeAddress("redis://192.168.31.101:7004")
                . addNodeAddress("redis://192.168.31.101:7005")
                . addNodeAddress("redis://192.168.31.101:7006")
            RedissonClient redisson = Redisson.create(config)
            RLock lock=redisson.getLock("anyLock");
            lock.lock();
            lock.unlock(); 
    

    就是这么简单,我们只需要通过它的api中的lock和unlock即可完成分布式锁,他帮我们考虑了很多细节:

    • redisson所有指令都通过lua脚本执行,redis支持lua脚本原子性执行

    • redisson设置一个key的默认过期时间为30s,如果某个客户端持有一个锁超过了30s怎么办?

      redisson中有一个watchdog的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔10秒帮你把key的超时时间重新延后30s

      这样的话,就不会出现业务没执行完而key过期了,其他线程获取到锁的问题了。

    • redisson的“看门狗”逻辑保证了没有死锁发生。

      (如果机器宕机了,看门狗也就没了。此时就不会延长key的过期时间,到了30s之后就会自动过期了,其他线程可以获取到锁)

    image

    这里稍微贴出来其实现代码:

    这里的代码请看下面参考连接中的

    另外,redisson还提供了对redlock算法的支持,

    它的用法也很简单:

    这里的代码请看下面参考连接中的

    小结:

    本节分析了使用redis作为分布式锁的具体落地方案

    以及其一些局限性

    然后介绍了一个redis的客户端框架redisson,

    这也是我推荐大家使用的,

    比自己写代码实现会少care很多细节。

    基于zookeeper实现分布式锁

    常见的分布式锁实现方案里面,除了使用redis来实现之外,使用zookeeper也可以实现分布式锁。

    在介绍zookeeper(下文用zk代替)实现分布式锁的机制之前,先粗略介绍一下zk是什么东西:

    Zookeeper是一种提供配置管理、分布式协同以及命名的中心化服务。

    zk的模型是这样的:zk包含一系列的节点,叫做znode,就好像文件系统一样每个znode表示一个目录,然后znode有一些特性:

    • 有序节点:假如当前有一个父节点为/lock,我们可以在这个父节点下面创建子节点;

      zookeeper提供了一个可选的有序特性,例如我们可以创建子节点“/lock/node-”并且指明有序,那么zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号

      也就是说,如果是第一个创建的子节点,那么生成的子节点为/lock/node-0000000000,下一个节点则为/lock/node-0000000001,依次类推。

    • 临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,zookeeper会自动删除该节点。

    • 事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端。当前zookeeper有如下四种事件:

    • 节点创建

    • 节点删除

    • 节点数据修改

    • 子节点变更

    基于以上的一些zk的特性,我们很容易得出使用zk实现分布式锁的落地方案:

    1. 使用zk的临时节点和有序节点,每个线程获取锁就是在zk创建一个临时有序的节点,比如在/lock/目录下。

    2. 创建节点成功后,获取/lock目录下的所有临时节点,再判断当前线程创建的节点是否是所有的节点的序号最小的节点

    3. 如果当前线程创建的节点是所有节点序号最小的节点,则认为获取锁成功。

    4. 如果当前线程创建的节点不是所有节点序号最小的节点,则对节点序号的前一个节点添加一个事件监听。

      比如当前线程获取到的节点序号为/lock/003,然后所有的节点列表为[/lock/001,/lock/002,/lock/003],则对/lock/002这个节点添加一个事件监听器。

    如果锁释放了,会唤醒下一个序号的节点,然后重新执行第3步,判断是否自己的节点序号是最小。

    比如/lock/001释放了,/lock/002监听到时间,此时节点集合为[/lock/002,/lock/003],则/lock/002为最小序号节点,获取到锁。

    整个过程如下:

    image

    具体的实现思路就是这样,至于代码怎么写,这里比较复杂就不贴出来了。

    Curator介绍

    Curator是一个zookeeper的开源客户端,也提供了分布式锁的实现。

    他的使用方式也比较简单:

    InterProcessMutex interProcessMutex =  new InterProcessMutex(client,"/anyLock");`
    interProcessMutex.acquire();`
    interProcessMutex.release();`
    

    其实现分布式锁的核心源码如下:

    这里的代码请看下下面的连接

    其实curator实现分布式锁的底层原理和上面分析的是差不多的。这里我们用一张图详细描述其原理:

    image

    小结:

    本节介绍了zookeeperr实现分布式锁的方案以及zk的开源客户端的基本使用,简要的介绍了其实现原理。

    两种方案的优缺点比较

    学完了两种分布式锁的实现方案之后,本节需要讨论的是redis和zk的实现方案中各自的优缺点。

    对于redis的分布式锁而言,它有以下缺点:

    • 它获取锁的方式简单粗暴,获取不到锁直接不断尝试获取锁,比较消耗性能。

    • 另外来说的话,redis的设计定位决定了它的数据并不是强一致性的,在某些极端情况下,可能会出现问题。锁的模型不够健壮

    • 即便使用redlock算法来实现,在某些复杂场景下,也无法保证其实现100%没有问题,关于redlock的讨论可以看How to do distributed locking

    • redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。

    但是另一方面使用redis实现分布式锁在很多企业中非常常见,而且大部分情况下都不会遇到所谓的“极端复杂场景”

    所以使用redis作为分布式锁也不失为一种好的方案,最重要的一点是redis的性能很高,可以支撑高并发的获取、释放锁操作。

    对于zk分布式锁而言:

    • zookeeper天生设计定位就是分布式协调,强一致性。锁的模型健壮、简单易用、适合做分布式锁。

    • 如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。

    但是zk也有其缺点:如果有较多的客户端频繁的申请加锁、释放锁,对于zk集群的压力会比较大。

    小结:

    综上所述,redis和zookeeper都有其优缺点。我们在做技术选型的时候可以根据这些问题作为参考因素。

    作者的一些建议

    通过前面的分析,实现分布式锁的两种常见方案:redis和zookeeper,他们各有千秋。应该如何选型呢?

    就个人而言的话,我比较推崇zk实现的锁:

    因为redis是有可能存在隐患的,可能会导致数据不对的情况。但是,怎么选用要看具体在公司的场景了。

    如果公司里面有zk集群条件,优先选用zk实现,但是如果说公司里面只有redis集群,没有条件搭建zk集群。

    那么其实用redis来实现也可以,另外还可能是系统设计者考虑到了系统已经有redis,但是又不希望再次引入一些外部依赖的情况下,可以选用redis。

    这个是要系统设计者基于架构的考虑了

    思考

    加锁是为了资源不能同时被共享,那么队列是单个消费的,能保证资源不被共享。 那么能使用队列代替吗?

    参考

    分布式锁
    redLock
    基于Redis的分布式锁到底安全吗(上)
    Redlock(redis分布式锁)原理分析

    相关文章

      网友评论

        本文标题:面试不懂分布式锁?看这篇文章就够了

        本文链接:https://www.haomeiwen.com/subject/pklklctx.html