【技术派后端篇】Redis分布式锁:原理、实践与应用

在当今的高并发系统中,分布式锁是保障数据一致性和系统稳定性的重要手段。今天,我们就来深入探讨一下Redis分布式锁,揭开它神秘的面纱。

1 本地锁与分布式锁的区别

在Java开发的早期阶段,我们接触过synchronizedLock锁,这些都属于本地锁。本地锁的特点是仅对当前节点有效,举个例子:假设我们有两个节点node A和node B,当node A获取了本地锁时,node B依然可以获取相同的锁。
在这里插入图片描述

如果我们的服务仅部署了一个节点,本地锁是完全能够满足需求的。但随着业务的发展,为了应对高并发、实现高可用和高性能,很多系统会采用多节点(集群部署)的方式。此时,本地锁就显得力不从心了,分布式锁便应运而生。

为什么本地锁锁不住?

  1. 锁范围有限:有多个服务实例时,本地锁只在自己的实例里起作用。像synchronized ,不同实例的锁对象(字节码 )不一样。大量用户查文章详情,缓存没数据时,不同实例的线程都能去连MySQL,本地锁拦不住。
  2. 跨进程管不了:服务分布式部署,在不同JVM进程里。本地锁只能管自己进程内的线程,别的进程线程来访问MySQL ,它管不了。
  3. 高并发顶不住:高并发时很多请求同时来,本地锁在单个JVM里抢锁很厉害,而且它也没法阻止其他JVM进程的线程访问MySQL ,数据库还是可能被大量请求弄“挂” 。

分布式锁的锁对象不在服务实例中,而是在服务实例的外部。分布式锁的核心思想是,当一个节点获取到锁后,其他节点无法获取该锁,从而保证了在分布式环境下的资源同步访问。

分布式锁的多种实现方式:

  • Redis分布式锁;
  • Zookeeper分布式锁;
  • MySQL分布式锁。

2 Redis分布式锁、Zookeeper分布式锁与MySQL分布式锁的差异

谈到分布式锁,就不得不提到CAP理论,即强一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance),三者只能选其二。

  1. Redis分布式锁 :它追求的是高可用性和分区容错性。Redis在写入主节点数据后,会立即返回成功,而不关心异步主节点同步从节点数据是否成功。由于Redis是基于内存的,其性能极高,官方给出的指标是每秒可达到10W的吞吐量。适用于对性能要求较高、允许一定数据延迟一致性的场景,比如一般的电商商品浏览、秒杀活动中的部分非核心数据校验等场景。
  2. Zookeeper分布式锁 :Zookeeper更侧重于强一致性和分区容错性。在写入主节点数据后,Zookeeper会等待从节点同步完数据后才返回成功,这在一定程度上牺牲了可用性。常用于对数据一致性要求极高的场景,例如金融行业的账务处理等场景。
  3. MySQL分布式锁
    • 实现原理:通常利用数据库自身的事务机制和行锁、表锁等特性来实现。比如通过在特定表中插入或更新特定记录,并利用事务的原子性来获取锁。若插入或更新成功则表示获取锁成功,否则获取失败。
    • 性能方面:相比Redis基于内存的操作,MySQL分布式锁由于涉及磁盘I/O(即使有缓存机制 ),在高并发场景下性能相对较低。例如在大量短时间内的锁竞争场景中,Redis可能轻松应对每秒数万甚至数十万的请求,而MySQL可能每秒只能处理数千请求。
    • 一致性与可用性:它在一定程度上能保证数据的强一致性,因为基于事务机制,数据操作要么全部成功,要么全部失败。但在可用性方面,当数据库出现故障(如主库宕机 )时,可能导致锁服务不可用,且恢复时间相对较长。而且,若锁表等关键资源出现瓶颈,会严重影响整个系统的可用性。
    • 适用场景:适用于对一致性要求较高,并发量不是特别极端高,且业务逻辑相对复杂,需要借助数据库事务特性来保证数据完整性的场景,比如一些企业内部的业务流程审批系统,涉及多步骤数据更新和状态转换,利用MySQL分布式锁可以结合事务更好地控制流程顺序和数据一致性。

综合考虑,为了追求更好的用户体验度,在高并发且对性能要求较高、对数据一致性有一定容忍度的场景下,很多时候会选择Redis分布式锁来实现;而在对一致性要求极高,对性能要求相对没那么苛刻的场景下,可能会选择Zookeeper分布式锁或MySQL分布式锁。

3 使用Redis分布式锁的背景

以查询文章详情为例:
在这里插入图片描述

用户根据articleId查询文章详情时,正常流程是先查询缓存,如果缓存中有数据,直接返回;如果缓存中没有数据,则需要到MySQL中查询。在并发量不高的情况下,这个流程没有问题。但当并发量很高时,就会出现问题。假设缓存中没有数据,大量用户会同时访问DB层的MySQL。而MySQL的资源相对珍贵,且性能不如Redis,很容易导致MySQL被打宕机,进而影响整个服务。

为了解决这个问题,当大量用户同时访问同一篇文章时,我们只允许一个用户去MySQL中获取数据。由于服务是集群化部署的,所以需要用到Redis分布式锁。通过加锁的方式,可以有效地保护DB层数据库,保证系统的高可用性。
在这里插入图片描述

4 Redis分布式锁的实现方式

4.1 Redis实现分布式锁

  • 项目仓库(GitHub):https://github.com/itwanger/paicoding
  • 项目仓库(码云):https://gitee.com/itwanger/paicoding
  • 分支:origin/feature/redis_distributed_lock_20230531

4.1.1 第一种方式:setIfAbsent(key,value,time)

使用redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS),对应的Redis命令是set key value EX time NX。这是一个复合操作,由setNx + setEx组成,底层采用lua脚本来保证原子性,要么全部成功,否则加锁失败。其含义是:如果key不存在,则加锁成功,返回true;否则加锁失败,返回false 。
在这里插入图片描述
代码实现:

/*** Redis分布式锁第一种方法** @param articleId* @return ArticleDTO*/
private ArticleDTO checkArticleByDBOne(Long articleId) {String redisLockKey =RedisConstant.REDIS_PAI + RedisConstant.REDIS_PRE_ARTICLE + RedisConstant.REDIS_LOCK + articleId;ArticleDTO article = null;// 加分布式锁:此时value为null,时间为90s(结合自己场景设置合适过期时间)Boolean isLockSuccess = redisUtil.setIfAbsent(redisLockKey, null, 90L);if (isLockSuccess) {// 加锁成功可以访问数据库article = articleDao.queryArticleDetail(articleId);} else {try {// 短暂睡眠,为了让拿到锁的线程有时间访问数据库拿到数据后set进缓存,// 这样在自旋时就能够从缓存中拿到数据;注意时间依旧结合自己实际情况Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}// 加锁失败采用自旋方式重新拿取数据this.queryDetailArticleInfo(articleId);}return article;
}

这种方式的主要逻辑是:当缓存中没有数据时,开始加锁,加锁成功则允许访问数据库,加锁失败则自旋重新访问。但它存在一个缺点,虽然在setIfAbsent中设置了过期时间,但可能会出现业务执行完之后,锁还被持有的情况。虽然Redis有淘汰策略,但这种情况还是不建议出现,因为Redis缓存资源非常重要,正确的做法应该是业务执行完后直接释放锁。

4.1.2 第二种方式:setIfAbsent(key,value,time)的优化

为了解决第一种方式中锁不能及时释放的问题,我们在业务执行完毕之后(增加finally块)立即删除key值。
代码实现:

/*** Redis分布式锁第二种方法** @param articleId* @return ArticleDTO*/
private ArticleDTO checkArticleByDBTwo(Long articleId) {String redisLockKey =RedisConstant.REDIS_PAI + RedisConstant.REDIS_PRE_ARTICLE + RedisConstant.REDIS_LOCK + articleId;ArticleDTO article = null;Boolean isLockSuccess = redisUtil.setIfAbsent(redisLockKey, null, 90L);try {if (isLockSuccess) {article = articleDao.queryArticleDetail(articleId);} else {Thread.sleep(200);this.queryDetailArticleInfo(articleId);}} catch (InterruptedException e) {e.printStackTrace();} finally {// 和第一种方式相比增加了finally中删除keyRedisClient.del(redisLockKey);}return article;}

但这种方式也存在问题,比如线程A获取到锁并正在执行业务,还未执行完成时,锁的过期时间到了,该锁被释放。此时线程B可以获取该锁并执行业务逻辑,而当线程A执行完成后,它释放的将是线程B的锁,即释放了别人的锁。
在这里插入图片描述

4.1.3 第三种方式:改进误释放锁的问题

为了解决误释放他人锁的情况,我们在加锁时设置一个value值,然后在释放锁前判断给key的value是否和前面设置的value值相等,相等则说明是自己的锁,可以删除;否则是别人的锁,不能删除。
在这里插入图片描述
代码实现:

/*** Redis分布式锁第三种方法** @param articleId* @return ArticleDTO*/
private ArticleDTO checkArticleByDBThree(Long articleId) {String redisLockKey =RedisConstant.REDIS_PAI + RedisConstant.REDIS_PRE_ARTICLE + RedisConstant.REDIS_LOCK + articleId;// 设置value值,保证不误删除他人锁String value = RandomUtil.randomString(6);Boolean isLockSuccess = redisUtil.setIfAbsent(redisLockKey, value, 90L);ArticleDTO article = null;try {if (isLockSuccess) {article = articleDao.queryArticleDetail(articleId);} else {Thread.sleep(200);this.queryDetailArticleInfo(articleId);}} catch (InterruptedException e) {e.printStackTrace();} finally {// 这种先get出value,然后再比较删除;这无法保证原子性,为了保证原子性,采用了lua脚本/*String redisLockValue = RedisClient.getStr(redisLockKey);if (!ObjectUtils.isEmpty(redisLockValue) && StringUtils.equals(value, redisLockValue)) {RedisClient.del(redisLockKey);}*/// 采用lua脚本来进行先判断,再删除;和上面的这种方式相比保证了原子性Long cad = redisLuaUtil.cad("pai_" + redisLockKey, value);log.info("lua 脚本删除结果:" + cad);}return article;}

不过,这种方式又带来了一个新问题,那就是过期时间的值该如何设置呢?

  • 如果时间设置过短,可能业务还未执行完毕,锁就已经过期被释放,其他线程可以拿到锁去访问DB,这就违背了我们加锁的初衷;
  • 如果时间设置过长,可能在加锁成功后还未执行到释放锁时,节点宕机了,那么在锁未过期的这段时间,其他线程无法获取锁。
  • 针对这个问题,我们可以写一个守护线程,每隔固定时间查看业务是否执行完毕,如果没有执行完毕,则延长其过期时间,即为锁续期

4.2 Redission实现分布式锁

Redission实现分布式锁的流程是:首先获取锁(get lock()),然后尝试加锁,加锁成功后执行下面的业务逻辑,执行完毕之后释放该分布式锁。
在这里插入图片描述

代码实现:

/*** Redis分布式锁第四种方法** @param articleId* @return ArticleDTO*/
private ArticleDTO checkArticleByDBFour(Long articleId) {ArticleDTO article = null;String redisLockKey =RedisConstant.REDIS_PAI + RedisConstant.REDIS_PRE_ARTICLE + RedisConstant.REDIS_LOCK + articleId;RLock lock = redissonClient.getLock(redisLockKey);//lock.lock();try {//尝试加锁,最大等待时间3秒,上锁30秒自动解锁if (lock.tryLock(3, 30, TimeUnit.SECONDS)) {article = articleDao.queryArticleDetail(articleId);} else {// 未获得分布式锁线程睡眠一下;然后再去获取数据Thread.sleep(200);this.queryDetailArticleInfo(articleId);}} catch (InterruptedException e) {e.printStackTrace();} finally {//判断该lock是否已经锁 并且 锁是否是自己的if (lock.isLocked() && lock.isHeldByCurrentThread()) {lock.unlock();}}return article;
}

Redission解决了Redis实现分布式锁中出现的锁过期问题和释放他人锁的问题。它还是可重入锁,内部机制是默认锁过期时间是30s,然后会有一个定时任务每10s去扫描一下该锁是否被释放,如果没有释放则延长至30s,这就是看门狗机制。如果请求没有获取到锁,那么它将通过while循环继续尝试加锁。

5 总结

通过本文,我们从原理到实践,详细介绍了Redis分布式锁的相关知识。我们了解了本地锁与分布式锁的区别,Redis分布式锁、Zookeeper分布式锁、MySQL分布式锁的差异,以及Redis分布式锁的几种实现方式。虽然Redission实现分布式锁基本解决了大部分问题,但当Redis是主从架构时,它也存在一些问题,比如线程A在master节点加锁后还未同步到slave节点,此时master节点挂了,线程B仍可以加锁,这涉及到高一致性问题,Redission无法解决。

如果想要解决高一致性问题,可以使用红锁或者zk锁,它们保证了高一致性,但不建议使用,因为为了保证高一致性,它们丢失了高可用性,对用户体验感不好,而且上述问题出现的几率不大,我们不能因为很小的问题而舍弃其高可用性。

希望本文能帮助大家更好地理解和应用Redis分布式锁,在实际项目中根据具体需求选择合适的分布式锁实现方式。

6 参考链接

  1. 技术派Redis分布式锁

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/77208.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

奥比中光tof相机开发学习笔记

针对奥比中光 tof相机,官方提供的资料如下ProcessOn Mindmap|思维导图 Orbbec SDK Python Wrapper基于Orbbec SDK进行设计封装,主要实现数据流接收,设备指令控制。下面就其开发适配进行如下总结: (1)系统配…

如何学习嵌入式

写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难,但我还是想去做! 本文写于:2025.04.16 请各位前辈能否给我提点建议,或者学习路线指导一下 STM32单片机学习总…

2025 年蓝桥杯 Java B 组真题解析分享

今年是我第二次参加蓝桥杯软件类Java B组的比赛,虽然赛前做了不少准备,但真正坐在考场上时,还是有种熟悉又紧张的感觉。蓝桥杯的题目一向以“基础创新”著称,今年也不例外,每道题都考验着我们对算法的理解、代码实现能…

Vue3服务器端渲染深度实践:架构、性能与全栈集成

一、SSR架构设计模式 1.1 架构模式选择矩阵 维度CSRSSR混合渲染首次内容渲染(FCP)慢(依赖JS执行)快(HTML直出)按路由动态选择SEO支持需预渲染原生支持关键页预渲染服务端压力低(静态托管)高(实时渲染)使用缓存中间层TTI(可交互时间)受限于JS体积需等待Hydration渐进式激活适用…

2025年泰迪杯数据挖掘竞赛B题论文首发+问题一二三四代码分享

料 基于穿戴装备的身体活动监测 摘要 随着科技的进步,加速度计,能够实时、准确地捕捉人体的动态变化,成为医学应用中的一个重要工具。本文将基于题目收集数据进行相关研究。 针对题目给出的数据集,我们首先进行数据清洗工作。首…

国内AI搜索平台与ChatGPT横向对比分析

一、核心技术差异 1、‌百度文小言‌ 基于文心大模型4.0升级,主打“新搜索”能力,支持多模态输入(语音、图片、视频)和富媒体搜索结果‌。 独有的“记忆个性化”功能可结合用户历史行为优化回答,并在医疗、教育等垂直…

安卓环境搭建开发工具下载Gradle下载

1.安装jdk(使用java语言开发安卓app) 核心库 java.lang java.util java.sq; java.io 2.安装开发工具(IDE)android studio https://r3---sn-2x3elnel.gvt1-cn.com/edgedl/android/studio/install/2023.3.1.18/android-studio-2023.3.1.18-windows.exe下载完成后一步一步安装即…

Python 趣味学习 -数据类型脱口秀速记公式 [特殊字符]

🎤 Python数据类型脱口秀速记公式 🐍 1️⃣ 四大金刚登场 "Set叔(无序洁癖)、Tuple爷(顽固老头)、List姐(百变女王)、Dict哥(万能钥匙)"2️⃣ 特性对比RAP 🎶 内存/作用域: 全局变量 → 函数内修改 → 可变(mutable)会…

单片机 | 基于51单片机的倾角测量系统设计

以下是一个基于51单片机的倾角测量系统设计详解,包含原理、公式和完整代码: 一、系统原理 核心器件:MPU6050(集成3轴加速度计+陀螺仪) 主控芯片:STC89C52RC(51单片机) 显示模块:LCD1602液晶 工作原理: 通过MPU6050采集XYZ三轴加速度数据,利用重力加速度分量计算俯仰…

2025年4月16日华为留学生笔试第二题200分

📌 点击直达笔试专栏 👉《大厂笔试突围》 💻 春秋招笔试突围在线OJ 👉 笔试突围OJ 02. 图书馆借阅管理系统 问题描述 卢小姐是一家大学图书馆的管理员,她需要开发一个简单的图书借阅管理系统来处理日常的图书流通操作。系统需要支持以下四种操作: in s:表示一本…

Linux通用一键换源脚本.sh - ubuntu、centos全自动更换国内源 - LinuxMirrors神器

效果 脚本 bash <(curl -sSL https://linuxmirrors.cn/main.sh) 来自 https://linuxmirrors.cn/ 截图 ending...

【Unity】JSON数据的存取

这段代码的结构是为了实现 数据的封装和管理&#xff0c;特别是在 Unity 中保存和加载玩家数据时。以下是对代码设计的逐步解释&#xff1a; 1. PlayerCoin 类 PlayerCoin 是一个简单的数据类&#xff0c;用于表示单个玩家的硬币信息。它包含以下字段&#xff1a; count&…

python实现音视频下载器

一、环境准备 确保当前系统已安装了wxPython 、 yt-dlp 和FFmpeg。当前主要支持下载youtube音视频 1、安装wxPython pip install wxPython2、安装yt-dp pip install wxPython yt-dlp3、安装FFmpeg 在Windows 10上通过命令行安装FFmpeg&#xff0c;最简便的方式是使用包管理…

使用 vxe-table 来格式化任意的金额格式,支持导出与复制单元格格式到 excel

使用 vxe-table 来格式化任意的金额格式&#xff0c;支持导出与复制单元格格式到 excel 查看官网&#xff1a;https://vxetable.cn gitbub&#xff1a;https://github.com/x-extends/vxe-table gitee&#xff1a;https://gitee.com/x-extends/vxe-table 安装 npm install vx…

知识图谱 数据准备

任何类型的数据格式都可以用于构建知识图谱&#xff0c;只要能够从中提取出实体&#xff08;Entities&#xff09;、关系&#xff08;Relationships&#xff09;和属性&#xff08;Attributes&#xff09;。但实际操作中&#xff0c;不同数据格式的处理难度、工具支持和效率差异…

Docker 设置镜像源后仍无法拉取镜像问题排查

#记录工作 Windows系统 在使用 Docker 的过程中&#xff0c;许多用户会碰到设置了国内镜像源后&#xff0c;依旧无法拉取镜像的情况。接下来&#xff0c;记录了操作要点以及问题排查方法&#xff0c;帮助我们顺利解决这类问题。 Microsoft Windows [Version 10.0.27823.1000…

如何对Flutter应用程序进行单元测试

Flutter单元测试完全指南&#xff1a;从基础到高级实践 面试求职资源 面试试题小程序&#xff1a;涵盖测试基础、Linux操作系统、MySQL数据库、Web功能测试、接口测试、APPium移动端测试、Python知识、Selenium自动化测试相关、性能测试、计算机网络知识、Jmeter、HR面试等内…

go中我遇到的问题总结

go问题总结 1 - go中的nil等于java中的null吗 在 Go 和 Java 中,nil 和 null 都用于表示“空值”,但它们的实现和使用方式有所不同。 以下是 Go 中的 nil 和 Java 中的 null 之间的对比: 1. Go 中的 nil 在 Go 中,nil 是一个预定义的常量,表示零值。它的行为根据数据类…

【android telecom 框架分析 01】【基本介绍 2】【BluetoothPhoneService为何没有源码实现】

1. 背景 我们会在很多资料上看到 BluetoothPhoneService 类&#xff0c;但是我们在实际 aosp 中确找不到具体的实现&#xff0c; 这是为何&#xff1f; 这是一个很好的问题&#xff01;虽然在车载蓝牙电话场景中我们经常提到类似 BluetoothPhoneService 的概念&#xff0c;但…

微机控制电液伺服汽车减震器动态试验系统

微机控制电液伺服汽车减震器动态试验系统&#xff0c;用于对汽车筒式减震器、减震器台架、驾驶室减震装置、发动机悬置软垫总成、发动机前置楔形支撑总成等的示功图试验、速度特性试验。 主要的技术参数&#xff1a; 1、最大试验力&#xff1a;5kN&#xff1b; 2、试验力测量精…