当前位置: 首页 > article >正文

Redis的分布式锁分析

系列文章目录

Java项目对接redis,客户端是选Redisson、Lettuce还是Jedis?


在这里插入图片描述

开发同学都知道,在分布式系统中,为了保证多个机器并发操作的数据一致性,常常需要使用分布式锁机制。常用的比如数据库锁,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集群而言,写操作则是要超过一半的实例成功才行,具体步骤如下:

  1. 获取当前时间戳。
  2. 依次向多个Redis节点发送 SETNX 命令,尝试在每个节点上创建一个带有过期时间(比如10秒)的锁。锁的键是一个唯一标识符,值是当前节点的标识符。
  3. 获取锁的操作还有一个超时时间,它要远小于锁的过期时间,一般是几十毫秒量级。客户端在向某个Redis节点获取锁失败以后,不管是超时还是真的失败了,应该立即尝试下一个Redis节点。
  4. 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间,计算成功获取锁的节点数(Quorum)。如果成功获取锁的节点数小于半数,则认为获取锁失败,返回错误。
  5. 如果成功获取锁的节点数大于等于半数,且获取锁的时间小于锁的过期时间,则认为获取锁成功。
  6. 如果获取锁成功,则返回锁的标识符和过期时间。如果获取锁失败,则在所有节点上通过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 或其他能保证一致性的组件,这些更适合作为分布式锁的基础。

http://www.kler.cn/a/395541.html

相关文章:

  • Elasticsearch集群和Kibana部署流程
  • Kettle配置数据源错误“Driver class ‘org.gjt.mm.mysql.Driver‘ could not be found”解决记录
  • react + ts定义接口类型写法
  • 基于 Python Django 的二手房间可视化系统分析
  • C++单例模式与多例模式
  • 文献解读-DNAscope: High accuracy small variant calling using machine learning
  • 【人工智能】Transformers之Pipeline(二十三):文档视觉问答(document-question-answering)
  • 【MySQL 保姆级教学】详细讲解视图--(15)
  • 五、函数封装及调用、参数及返回值、作用域、匿名函数、立即执行函数
  • 利用OpenAI进行测试需求分析——从电商网站需求到测试用例的生成
  • 移动端异构运算技术 - GPU OpenCL 编程(基础篇)
  • 论文笔记(五十六)VIPose: Real-time Visual-Inertial 6D Object Pose Tracking
  • Hadoop高可用集群工作原理
  • WSADATA 关键字详细介绍
  • 深度学习之循环神经网络(RNN)
  • 怎样选择合适的服务器租用呢?
  • Array数组方法
  • 【大数据】MySQL与Elasticsearch的对比分析:如何选择适合的查询解决方案
  • TCP为什么需要三次握手和四次挥手,有哪些需要注意的地方?
  • Pandas 数据结构
  • CCI3.0-HQ:用于预训练大型语言模型的高质量大规模中文数据集
  • pytorch中数据和模型都要部署在cuda上面
  • ctfshow-web入门-JWT(web345-web350)
  • 电动车租赁支付宝免押小程序开发方案php+uniapp
  • vue项目PC端和移动端实现在线预览pptx文件
  • YOLOv7-0.1部分代码阅读笔记-metrics.py