深度剖析:Redis分布式锁到底安全吗?看完这篇文章彻底懂了!

本文深入探讨了Redis分布式锁的安全性问题,包括死锁、过期时间评估和锁被误释放等,并介绍了Redis作者Antirez提出的Redlock方案及其与分布式专家Martin的争论。文章指出,Redlock需要在多数Redis实例上加锁且耗时小于锁过期时间以确保安全,但Martin质疑其依赖于同步时钟,可能导致锁失效。另外,对比了Zookeeper实现的分布式锁,发现其也无法在极端情况下保证安全性。文章最后提出了将分布式锁与fencing token方案结合的建议,以提高正确性。
摘要由CSDN通过智能技术生成

微信搜索关注「水滴与银弹」公众号,第一时间获取优质技术干货。7年资深后端研发,给你呈现不一样的技术视角。

大家好,我是 Kaito。

这篇文章我想和你聊一聊,关于 Redis 分布式锁的「安全性」问题。

Redis 分布式锁的话题,很多文章已经写烂了,我为什么还要写这篇文章呢?

因为我发现网上 99% 的文章,并没有把这个问题真正讲清楚。导致很多读者看了很多文章,依旧云里雾里。例如下面这些问题,你能清晰地回答上来吗?

  • 基于 Redis 如何实现一个分布式锁?
  • Redis 分布式锁真的安全吗?
  • Redis 的 Redlock 有什么问题?一定安全吗?
  • 业界争论 Redlock,到底在争论什么?哪种观点是对的?
  • 分布式锁到底用 Redis 还是 Zookeeper?
  • 实现一个有「容错性」的分布式锁,都需要考虑哪些问题?

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

读完这篇文章,你不仅可以彻底了解分布式锁,还会对「分布式系统」有更加深刻的理解。

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

为什么需要分布式锁?

在开始讲分布式锁之前,有必要简单介绍一下,为什么需要分布式锁?

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

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

例如,现在的业务应用通常都是微服务架构,这也意味着一个应用会部署多个进程,那这多个进程如果需要修改 MySQL 中的同一行记录时,为了避免操作乱序导致数据错误,此时,我们就需要引入「分布式锁」来解决这个问题了。

想要实现分布式锁,必须借助一个外部系统,所有进程都去这个系统上申请「加锁」。

而这个外部系统,必须要实现「互斥」的能力,即两个请求同时进来,只会给一个进程返回成功,另一个返回失败(或等待)。

这个外部系统,可以是 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

这个逻辑非常简单,整体的路程就是这样:

但是,它存在一个很大的问题,当客户端 1 拿到锁后,如果发生下面的场景,就会造成「死锁」:

  1. 程序处理业务逻辑异常,没及时释放锁
  2. 进程挂了,没机会释放锁

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

怎么解决这个问题呢?

如何避免死锁?

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

在 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 条命令,有没有可能只执行了第一条,第二条却「来不及」执行的情况发生呢?例如:

  1. SETNX 执行成功,执行 EXPIRE 时由于网络问题,执行失败
  2. SETNX 执行成功,Redis 异常宕机,EXPIRE 没有机会执行
  3. 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 操作共享资源的时间,「超过」了锁的过期时间,锁被「自动释放」
  3. 客户端 2 加锁成功,开始操作共享资源
  4. 客户端 1 操作共享资源完成,释放锁(但释放的是客户端 2 的锁)

看到了么,这里存在两个严重的问题:

  1. 锁过期:客户端 1 操作共享资源耗时太久,导致锁被自动释放,之后被客户端 2 持有
  2. 释放别人的锁:客户端 1 操作共享资源完成后,却又释放了客户端 2 的锁

导致这两个问题的原因是什么?我们一个个来看。

第一个问题,可能是我们评估操作共享资源的时间不准确导致的。

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

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

这样确实可以「缓解」这个问题,降低出问题的概率,但依旧无法「彻底解决」问题。

为什么?

原因在于,客户端在拿到锁之后,在操作共享资源时,遇到的场景有可能是很复杂的,例如,程

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值