您好,欢迎来到12图资源库!分享精神,快乐你我!我们只是素材的搬运工!!
  • 首 页
  • 当前位置:首页 > 开发 > WEB开发 >
    深度剖析:Redis散布式锁安全吗?
    时间:2021-08-03 12:09 来源:网络整理 作者:网络 浏览:收藏 挑错 推荐 打印

     

    深度剖析:Redis散布式锁安全吗?

    前言

    Redis 散布式锁的话题,很多文章曾经写烂了,但是小编发现网上 99% 的文章,并没有把这个成绩真正讲清楚。招致很多读者看了很多文章,照旧云里雾里。例如下面这些成绩,你能明晰地回答下去吗?

    基于 Redis 如何完成一个散布式锁?

    Redis 散布式锁真的安全吗?

    Redis 的 Redlock 有什么成绩?一定安全吗?

    业界争论 Redlock,究竟在争论什么?哪种观念是对的?

    散布式锁究竟用 Redis 还是 Zookeeper?

    完成一个有「容错性」的散布式锁,都需求思索哪些成绩?

    这篇文章,我就来把这些成绩彻底讲清楚。

    读完这篇文章,你不只可以彻底了解散布式锁,还会对「散布式系统」有愈加深入的了解。

    文章有点长,但干货很多,希望你可以耐烦读完。

    深度剖析:Redis散布式锁安全吗?

    一、为什么需求散布式锁?

    在末尾讲散布式锁之前,有必要复杂引见一下,为什么需求散布式锁?

    与散布式锁相对应的是「单机锁」,我们在写多线程顺序时,避免同时操作一个共享变量产生数据成绩,通常会运用一把锁来「互斥」,以保证共享变量的正确性,其运用范围是在「同一个进程」中。

    假设换做是多个进程,需求同时操作一个共享资源,如何互斥呢?

    例如,如今的业务运用通常都是微效劳架构,这也意味着一个运用会部署多个进程,那这多个进程假设需求修正 MySQL 中的同一行记载时,为了避免操作乱序招致数据错误,此时,我们就需求引入「散布式锁」来处置这个成绩了。

    深度剖析:Redis散布式锁安全吗?

    想要完成散布式锁,必须借助一个外部系统,一切进程都去这个系统上央求「加锁」。

    而这个外部系统,必需要完成「互斥」的才能,即两个央求同时出去,只会给一个进程前往成功,另一个前往失败(或等候)。

    这个外部系统,可以是 MySQL,也可以是 Redis 或 Zookeeper。但为了追求更好的功用,我们通常会选择运用 Redis 或 Zookeeper 来做。

    下面我就以 Redis 为主线,由浅入深,带你深度剖析一下,散布式锁的各种「安全性」成绩,帮你彻底了解散布式锁。

    二、散布式锁怎样完成?

    我们从最复杂的末尾讲起。

    想要完成散布式锁,必需要求 Redis 有「互斥」的才能,我们可以运用 SETNX 命令,这个命令表示SET if Not eXists,即假设 key 不存在,才会设置它的值,否则什么也不做。

    两个客户端进程可以执行这个命令,到达互斥,就可以完成一个散布式锁。

    客户端 1 央求加锁,加锁成功:

    127.0.0.1:6379> SETNX lock 1 

    (integer) 1     // 客户端1,加锁成功 

    客户端 2 央求加锁,由于它后抵达,加锁失败:

    127.0.0.1:6379> SETNX lock 1 

    (integer) 0     // 客户端2,加锁失败 

    此时,加锁成功的客户端,就可以去操作「共享资源」,例如,修正 MySQL 的某一行数据,或许调用一个 API 央求。

    操作完成后,还要及时释放锁,给后来者让出操作共享资源的时机。如何释放锁呢?

    也很复杂,直接运用 DEL 命令删除这个 key 即可:

    127.0.0.1:6379> DEL lock // 释放锁 

    (integer) 1 

    这个逻辑十分复杂,全体的路程就是这样:

    深度剖析:Redis散布式锁安全吗?

    但是,它存在一个很大的成绩,当客户端 1 拿到锁后,假设发作下面的场景,就会形成「死锁」:

    顺序处置业务逻辑异常,没及时释放锁

    进程挂了,没时机释放锁

    这时,这个客户端就会不断占用这个锁,而其它客户端就「永远」拿不到这把锁了。

    怎样处置这个成绩呢?

    三、如何避免死锁?

    我们很容易想到的方案是,在央求锁时,给这把锁设置一个「租期」。

    在 Redis 中完成时,就是给这个 key 设置一个「过时时间」。这里我们假定,操作共享资源的时间不会超过 10s,那么在加锁时,给这个 key 设置 10s 过时即可:

    127.0.0.1:6379> SETNX lock 1    // 加锁 

    (integer) 1 

    127.0.0.1:6379> EXPIRE lock 10  // 10s后自动过时 

    (integer) 1 

    这样一来,无论客户端能否异常,这个锁都可以在 10s 后被「自动释放」,其它客户端照旧可以拿到锁。

    但这样真的没成绩吗?

    还是有成绩。

    如今的操作,加锁、设置过时是 2 条命令,有没有能够只执行了第一条,第二条却「来不及」执行的状况发作呢?例如:

    SETNX 执行成功,执行 EXPIRE 时由于网络成绩,执行失败

    SETNX 执行成功,Redis 异常宕机,EXPIRE 没无时机执行

    SETNX 执行成功,客户端异常崩溃,EXPIRE 也没无时机执行

    总之,这两条命令不能保证是原子操作(一同成功),就有潜在的风险招致过时时间设置失败,照旧发作「死锁」成绩。

    怎样办?

    在 Redis 2.6.12 版本之前,我们需求想尽办法,保证 SETNX 和 EXPIRE 原子性执行,还要思索各种异常状况如何处置。

    但在 Redis 2.6.12 之后,Redis 扩展了 SET 命令的参数,用这一条命令就可以了:

    // 一条命令保证原子性执行 

    127.0.0.1:6379> SET lock 1 EX 10 NX 

    OK 

    这样就处置了死锁成绩,也比较复杂。

    我们再来看剖析下,它还有什么成绩?

    试想这样一种场景:

    客户端 1 加锁成功,末尾操作共享资源

    客户端 1 操作共享资源的时间,「超过」了锁的过时时间,锁被「自动释放」

    客户端 2 加锁成功,末尾操作共享资源

    客户端 1 操作共享资源完成,释放锁(但释放的是客户端 2 的锁)

    看到了么,这里存在两个严重的成绩:

    锁过时: 客户端 1 操作共享资源耗时太久,招致锁被自动释放,之后被客户端 2 持有

    释放别人的锁: 客户端 1 操作共享资源完成后,却又释放了客户端 2 的锁

    招致这两个成绩的缘由是什么?我们一个个来看。

    第一个成绩,能够是我们评价操作共享资源的时间不准确招致的。

    例如,操作共享资源的时间「最慢」能够需求 15s,而我们却只设置了 10s 过时,那这就存在锁提早过时的风险。

    过时时间太短,那增大冗余时间,例如设置过时时间为 20s,这样总可以了吧?

    这样确实可以「缓解」这个成绩,降低出成绩的概率,但照旧无法「彻底处置」成绩。

    为什么?

    缘由在于,客户端在拿到锁之后,在操作共享资源时,遇到的场景有能够是很复杂的,例如,顺序外部发作异常、网络央求超时等等。

    既然是「预估」时间,也只能是大致计算,除非你能预料并掩盖到一切招致耗时变长的场景,但这其实很难。

    有什么更好的处置方案吗?

    别急,关于这个成绩,我会在前面详细来讲对应的处置方案。

    我们继续来看第二个成绩。

    第二个成绩在于,一个客户端释放了其它客户端持有的锁。

    想一下,招致这个成绩的关键点在哪?

    重点在于,每个客户端在释放锁时,都是「无脑」操作,并没有反省这把锁能否还「归本人持有」,所以就会发作释放别人锁的风险,这样的解锁流程,很不「严谨」!

    如何处置这个成绩呢?

    四、锁被别人释放怎样办?

    处置办法是:客户端在加锁时,设置一个只要本人知道的「独一标识」出来。

    例如,可以是本人的线程 ID,也可以是一个 UUID(随机且独一),这里我们以 UUID 举例:

    // 锁的VALUE设置为UUID 

    127.0.0.1:6379> SET lock $uuid EX 20 NX 

    OK 

    这里假定 20s 操作共享时间完全足够,先不思索锁自动过时的成绩。

    之后,在释放锁时,要先判别这把锁能否还归本人持有,伪代码可以这么写:

    // 锁是本人的,才释放 

    if redis.get("lock") == $uuid: 

        redis.del("lock"

    这里释放锁运用的是 GET + DEL 两条命令,这时,又会遇到我们前面讲的原子性成绩了。

    客户端 1 执行 GET,判别锁是本人的

    客户端 2 执行了 SET 命令,强迫获取到锁(虽然发作概率比较低,但我们需求严谨地思索锁的安全性模型)

    客户端 1 执行 DEL,却释放了客户端 2 的锁

    由此可见,这两个命令还是必需要原子执行才行。

    怎样原子执行呢?Lua 脚本。

    我们可以把这个逻辑,写成 Lua 脚本,让 Redis 来执行。

    由于 Redis 处置每一个央求是「单线程」执行的,在执行一个 Lua 脚本时,其它央求必须等候,直到这个 Lua 脚本处置完成,这样一来,GET + DEL 之间就不会插入其它命令了。

    深度剖析:Redis散布式锁安全吗?

    安全释放锁的 Lua 脚本如下:

    // 判别锁是本人的,才释放 

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

    then 

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

    else 

        return 0 

    end 

    好了,这样一路优化,整个的加锁、解锁的流程就更「严谨」了。

    这里我们先小结一下,基于 Redis 完成的散布式锁,一个严谨的的流程如下:

    加锁:SET lock_key $unique_id EX $expire_time NX

    操作共享资源

    释放锁:Lua 脚本,先 GET 判别锁能否归属本人,再 DEL 释放锁

    深度剖析:Redis散布式锁安全吗?

    好,有了这个残缺的锁模型,让我们重新回到前面提到的第一个成绩。

    五、锁过时时间不好评价怎样办?

    锁过时时间不好评价怎样办?

    前面我们提到,锁的过时时间假设评价不好,这个锁就会有「提早」过时的风险。

    事先给的妥协方案是,尽量「冗余」过时时间,降低锁提早过时的概率。

    这个方案其实也不能完美处置成绩,那怎样办呢?

    (责任编辑:admin)