Redis的分布式锁分析
系列文章目录
Java项目对接redis,客户端是选Redisson、Lettuce还是Jedis?
由Redis引发的分布式锁探讨
- 系列文章目录
- 一、什么是分布式锁?
- 二、Redis分布式锁的几种实现
- 1. 简单分布式锁
- 2. Redlock
- 三、Redis 锁的问题
- 1. 互斥失效
- 2. 时钟偏移
- 四、与其他分布式锁相比
开发同学都知道,在分布式系统中,为了保证多个机器并发操作的数据一致性,常常需要使用分布式锁机制。常用的比如数据库锁,ZK锁,Reis锁等等,今天我们就探讨下Redis分布式锁,说说他的实现原理和应用场景,并介绍如何正确使用分布式锁来解决并发问题
📕作者简介:战斧,从事金融IT行业,有着多年一线开发、架构经验;爱好广泛,乐于分享,致力于创作更多高质量内容
📗本文收录于 Redis专栏 专栏,有需要者,可直接订阅专栏实时获取更新
📘高质量专栏 云原生、RabbitMQ、Spring全家桶 等仍在更新,欢迎指导
📙Zookeeper Redis kafka docker netty等诸多框架,以及架构与分布式专题即将上线,敬请期待
一、什么是分布式锁?
其实分布式锁并不是特指某个组件,而是对某些组件的用法,能够起到锁的作用。与我们之前讲的单体应用内部的锁,功能都是一致的。只不过在单体应用中,竞争锁的是各个线程,而在分布式场景下的锁,竞争锁的就是各个分布式应用了,如下:
所以说,分布式锁是一种机制,用于协调分布式系统中多个节点对共享资源的访问,保证资源的一致性和并发操作的正确性
二、Redis分布式锁的几种实现
一般我们利用Redis来作为分布式锁,有两种使用方式,一种是基于SETNX的简单锁
,这种锁只需要一个redis实例就好,简单易学。另一种则是更高可用的RedLock机制
,但这种方案要求至少要有一个Redis集群,成本更高,使用也更复杂。
1. 简单分布式锁
Redis提供了一系列的原子操作指令,例如SETNX
(SET if Not eXists - 如果不存在,则设置)可以实现原子的锁获取操作,保证在并发情况下只有一个客户端能够成功获取锁。
基于SETNX
命令实现的简单分布式锁:使用SETNX命令设置一个键值对,当键不存在时,设置成功并获取锁;当键存在时,表示锁已经被其他节点获取
// 成功设置返回1
setnx zhanfu aa
1
// 设置失败返回0
setnx zhanfu bb
0
// 删除锁
del zhanfu
1
基于此,抢锁使用setnx
, 释放锁使用del
。这样就完成了一个基本的锁功能。
但是我们不难发现如果我们最后忘记释放锁,或者释放锁的过程出现问题,那将导致这个锁一直存在。因此合理的操作应该是以NX参数
去使用SET
命令
// ex 20 指的是20秒后自动过期,这个值要结合业务来设定,至少要保证业务能做完
set zhanfu aa ex 20 nx
这里SET命令设置锁过期时间,这样我们就可以通过设置合理的过期时间来避免锁无法释放的问题。当然这样一来,又可能出现A业务执行时间过长,导致锁被自动删除后,再被B获取到,而A执行完业务代码上又执行删除锁,导致误删
的问题,所以一般上锁的值都是一个独有的uuid,而删除则是在Lua脚本里先判断锁是否仍由自己占有,再进行删除
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
2. Redlock
但是我们很明显能注意到,Redis的简单锁基于单实例Redis,在面对Redis集群时,Redis实例间一旦数据不一致,就很难处理,或者单实例宕机了,那就无法使用了,那么多实例的场景下该怎么办呢?
所以,Redlock
是一种针对多实例的分布式锁算法,原理基于Quorum
机制,即多数派机制,他的核心思想是通过在多个节点上创建相同的锁,并使用Redis的原子操作来保证原子性,看最后上锁的成功的节点数是否超过总节点数的1/2
Quorum机制的核心在于定义三个参数:N、W、R。其中,N表示副本的总数,W表示成功写入操作所需的副本数量,R表示成功读取操作所需的副本数量(设计时需保证W + R > N)。当进行写入操作时,只要W个副本成功写入,操作即被认为成功。读取操作时,只要R个副本返回数据,即可保证读取到的是最新的数据。
比方说一共有7个副本,一次成功更新了5个,那么至少需要读取3个副本的数据,才可以保证读到了最新的数据。
而对于Redis集群而言,写操作则是要超过一半的实例成功才行,具体步骤如下:
- 获取当前时间戳。
依次
向多个Redis节点发送SETNX
命令,尝试在每个节点上创建一个带有过期时间(比如10秒)的锁。锁的键是一个唯一标识符,值是当前节点的标识符。- 获取锁的操作还有一个超时时间,它要远小于锁的过期时间,一般是几十毫秒量级。客户端在向某个Redis节点获取锁失败以后,不管是超时还是真的失败了,应该立即尝试下一个Redis节点。
- 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间,计算成功获取锁的节点数(Quorum)。如果成功获取锁的节点数小于半数,则认为获取锁失败,返回错误。
- 如果成功获取锁的节点数大于等于半数,且获取锁的时间小于锁的过期时间,则认为获取锁成功。
- 如果获取锁成功,则返回锁的标识符和过期时间。如果获取锁失败,则在所有节点上通过LUA执行DEL命令,释放已创建的锁。
通过以上步骤,Redlock 算法可以比较可靠地在Redis集群里实现分布式锁。
三、Redis 锁的问题
1. 互斥失效
但是 Redlock
尽管在集群里实现了分布式锁,可不难看出,他的设计上仍有不少异常情况难以处理(有些问题单例Redis上也有,所以统称为Redis锁问题)。也曾引发起广泛的讨论,比如下面这种场景
- 客户端1在获得锁
- 客户端1之后发生了很长时间的GC pause,在此期间,它获得的锁过期了。
- 而客户端2获得了锁。
- 客户端1从GC pause中恢复过来的时候,它不知道自己持有的锁已经过期了
- 客户端1, 客户端2都认为自己获得了锁,进而都去操作共享数据了。
而这种场景下,原作者的意思是在对共享资源进行操作前,都进行一个版本校验来解决,也即带上一个token,然后通过CAS来确保操作共享资源时,自己的token是最新的。但问题是,如果共享资源可以提供原子性的CAS的能力,自己就具备了互斥能力,似乎就没必要使用分布式锁了。
2. 时钟偏移
另外一个显著的问题是, Redlock
需要依赖系统时间来计算锁的过期时间和判断锁的有效性。如果发生了时钟偏移(不同节点之间的时间存在较大的偏差),可能会导致锁的有效性错误判断。
尤其是针对一种时钟跳跃
的情况,就是某些节点的时钟突然产生了较大的变动,将直接影响到Redis的锁过期的问题,
对于这些问题,分布式系统专家Martin Kleppmann 的意见是:如果是为了效率(efficiency)而使用分布式锁,允许锁的偶尔失效,那么使用单Redis节点的锁方案就足够了,简单而且效率高。Redlock则是个过重的实现(heavyweight)。
如果是为了正确性(correctness)在很严肃的场合使用分布式锁,那么不要使用Redlock。它不是建立在异步模型上的一个足够强的算法,它对于系统模型的假设中包含很多危险的成分(对于timing)。而且,它没有一个机制能够提供fencing token。
也正由于体验不佳,我们前面提到过的Redission
客户端已经将 Redlock
的使用废弃了.
四、与其他分布式锁相比
除了Redis,常用的分布式锁还有 ZooKeeper
和 数据库
,严格的说分布式锁并不是某个组件,而是一个机制与用法,所以很多组件都能当作分布式锁来用,我们可以从这几个角度来考量:
– | 性能 | 可靠性 | 功能特性 | 使用场景 |
---|---|---|---|---|
Redis | 是一个内存数据库,因此读写速度非常快,适用于高并发场景 | 通过复制和持久化来确保数据的可靠性,即使发生故障,也可以通过故障转移来保证服务的可用性 | 提供了丰富的数据结构和功能,如发布/订阅、事务、Lua脚本 | 适用于高并发的场景,如秒杀、抢购等需要快速响应的业务 |
ZooKeeper | 通过主节点选举和写操作的持久化,保证了数据的一致性,但相对于Redis来说,性能较差 | 使用ZAB协议来提供高可用的分布式一致性。它的选举机制和写操作持久化确保了数据的一致性和可靠性 | 提供了分布式锁之外的更多功能,如配置管理、分布式队列等。它还可以用于分布式协调和命名服务 | 适用于需要强一致性和可靠性的分布式场景,如分布式事务、分布式锁 |
数据库 | 数据库的性能受到硬件和数据库引擎的影响,通常不如Redis和ZooKeeper | 数据库的可靠性取决于数据库引擎的复制和故障恢复机制 | 主要用于数据存储和查询,通常没有专门的分布式协调功能 | 适用于对数据一致性要求较高的场景,如金融交易系统、电子商务等 |
一般来讲,大部分场景下,我们选用分布式锁,最重要的考量还是一致性,这直接影响这个锁的正确性。然后才是性能及其他指标。所以如果从这个角度来考虑,
- 如果是一些非关键的业务,使用单点Redis 的 setNx,又或者集群环境,使用RedLock是可以的。前者简单易用,后者可靠性更好一点
- 如果是关键业务,尽管性能稍逊,但仍建议使用 ZK 或其他能保证一致性的组件,这些更适合作为分布式锁的基础。