【Java学习笔记】70:借助Redis实现分布式锁

2023-11-17

这节学习Java用Redis做分布式锁,来做秒杀场景卖货减库存的案例。

最原始的减库存写法

这里库存也存Redis里面,调减库存接口的时候判断一下大于0(还有库存)就拿出来减1。
在这里插入图片描述
这里StringRedisTemplate是Spring Boot对Redis的封装,27行和30行的写法就等同于注释里面的用Jedis的写法,就是去调Redis的GET和SET命令。

这样的代码中存在并发问题,在高并发的场景下,只要多个线程都执行读库存的操作,那么读出来的库存数目就是一样多的。比如三个线程都读出来200,然后都减少1,变成199,最后写回也是199,但是卖出了3件商品,这个就是典型的超卖问题

JVM级别的锁不能解决分布式场景问题

如果用synchronized给这段代码加锁,实际上是保证了在一个实例上多个线程访问这段代码,每次只有获取到锁的那个能执行,这个是JVM进程级别的锁,在不同的实例上(分布式场景下)就没法用这个锁来保证安全了。
在这里插入图片描述
比如下面这种,访问Nginx代理服务器,然后LB到两个tomcat实例上的场景。JVM级别的锁只能保证实例内部的线程之间按这个锁来一个一个访问这段代码,不同实例之间的还是没法保证。
在这里插入图片描述
相应的Nginx配置,可以看到流量被转发到了8080端口和8090端口的两个实例上:
在这里插入图片描述

使用Apache JMeter模拟高并发场景暴露问题

JMeter是Java开发常用的一个压力测试软件,可以给RD开发完成后自测,模拟高并发场景,暴露出可能存在的问题。

首先在测试计划(Test Plan)下添加一个线程组(Thread Group):
在这里插入图片描述
然后在线程组里添加一个HTTP Request:
在这里插入图片描述
在Path里配置要压测的接口:
在这里插入图片描述
回到Thread Group,在这里配置来告诉JMeter以什么样的压力来跑下面配的请求。

下图的配置含义是,发200个请求,在0秒内(表示这200个请求一次性发过去,为了模拟高并发所以这样配置瞬间的压力),循环压4次(4个200):
在这里插入图片描述
在压测之前,在HTTP Request下面添加一个聚合报告(Aggregate Report),这样就能得到测试的结果:
在这里插入图片描述
点击绿色运行按钮,选择聚合报告的保存位置,然后就开始压测了,完成后可以看到报告:
在这里插入图片描述
但是这次关注点不在这个报告,而是代码里打出来的日志,只要能看到两个实例中有打印出相同的库存数,就证明存在超卖的问题:
在这里插入图片描述
最后结果发现是存在超卖问题的,也证实了JVM锁不能解决分布式场景下需要加锁的问题。

Redis SETNX命令

使用Redis中的SETNX命令,仅当key不存在时设置,可以实现分布式锁:
在这里插入图片描述
和SET的区别就是,SET是在key存在时会覆盖,但是SETNX在key存在的时候会写入失败(也就可以用来表示获取锁失败)。

使用SETNX:实现分布式锁

设置成功时(返回true)表示加锁成功,可以执行业务逻辑;设置失败(返回false)时加锁失败,这里的处理是返回一个错误码(那么前端拿到后可以显示“服务器繁忙,请稍后再试”)这样的字样。

还需要注意在业务代码执行完毕之后,要把锁释放掉,所以要在Redis里把这个key清除掉。
在这里插入图片描述
使用SETNX来实现分布式锁的思想,也借助了Redis是单线程的这件事,在执行的时候都会排队执行,所以只有一个能设置成功,也就实现了“分布式环境下只有一个线程设置锁成功”这件事。

注:这里在加锁失败的时候会直接返回错误码,另一种处理方式是套一个循环,在加锁失败的时候把这个线程sleep一段时间,然后再去加锁。具体采用哪种处理方式还是看具体业务场景和端上需求了。

使用try-finally:解决“业务代码异常退出引起的解锁无法执行”问题

上面的写法中,如果业务代码里抛出异常了,那么设置锁之后线程在释放锁之前就终止了,这样导致其他线程一直没法加锁,所以这里可以用try来把业务代码套起来,然后在finally的部分去执行释放锁的操作:
在这里插入图片描述

设置超时时间:解决“机器宕机引起的解锁无法执行”问题

如果在业务代码执行的过程中机器宕机了, 那么后面的finally块还是无法执行,还是没法保证锁被正确释放,所以可以为加入的key设置一个超时时间,这样即使宕机了,只要超过这个时间还是会释放锁(下面的代码里是设置了10秒):
在这里插入图片描述

使用原子性操作:解决“设置key-设置超时之间宕机”问题

在上面的代码里,如果27行设置key和28行设置超时之间发生了宕机,那么还是会出现key一直存在,锁没法释放的问题,这里可以改用一个原子操作来解决:
在这里插入图片描述
在Redis里会作为一个原子操作来指定,同时成功或者同时失败,显然即使同时失败了也没有影响。

value设置唯一标识:极大缓解“释放别人的锁”问题

目前的代码里还是有小概率发生超卖问题,因为引入了超时时间,如果线程A中业务代码执行的时间超过了超时时间(比如有慢查询之类的),那么key会自动失效(这个时候线程A还没执行解锁,但是锁实际上已经被释放了)。这个时候大量的请求过来,假设线程B又获取到了锁开始了执行,这个时候线程A执行到了释放锁的位置,就把线程B的锁给释放掉了。

在这样的场景下,可能就会导致后续释放的都是别人的锁,这样就会引起代码执行的顺序完全不可控,只要并发量一直存在,极端情况下可能导致分布式锁的永久失效问题,导致这个问题被无限放大。

到目前位置,只是在用key来标识锁,现在要解决释放别人的锁的问题,只要用value来记录加锁的是谁就可以了(不要用原生的线程id,因为不同实例上线程id可能相同)。

下面的代码里用生成的UUID(发生重复的概率极低可以忽略不计,其实就可以认为一定不会发生重复)来模拟每个线程执行时候的客户端id,用来唯一标识加锁人的身份。
在这里插入图片描述

仍存在的小问题:分布式锁的解锁逻辑该怎么做

上面的代码里45到47行还是存在原子性的问题,比如线程A在45行判断是自己加的锁成功,紧接着(或者是发生了GC也可能导致两行代码执行之间停顿了一下)这个key的超时时间到了,这个锁被释放掉了,然后大量的并发请求过来,线程B加锁成功,这个时候A再去执行解锁,解的是线程B加的锁。也就是说这样写的话,“释放别人的锁”问题是依然存在的。

最好的方式是把判断和解锁做成一个原子操作,引起这个问题还是因为引入了超时时间,所以可以看一下这个超时时间应该怎么处理。

锁续命:对key的超时问题的处理

把超时时间延长,是一个治标不治本的办法,例如业务代码执行的是一个定时任务,那么就是会执行比较长的时间,是没法避免的(总是可能发生超时)。

一种处理方法是,在业务线程开始执行的同时开一个分线程,例如主线程超时时间是30秒,那么分线程可以设置定时任务,每过10秒钟就检查一下主线程有没有执行完退出(锁有没有被删除掉):如果执行完了就让自己也结束;如果没有执行完,就把超时时间重置。


从头来实现上面的这些,把所有的问题都考虑清楚是很困难的,市面上已经存在一些框架把这些事情做好了,比如Redisson。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【Java学习笔记】70:借助Redis实现分布式锁 的相关文章

  • 我是否需要安装 SQLite 才能使 SQLiteJDBC 正常工作?

    我想我只是没有 明白 如果我的计算机上尚未安装 SQLite 并且我想编写一个使用嵌入式数据库的 Java 应用程序 并且我将 SQLiteJDBC JAR 下载 导入到我的项目中 那么这就是我所需要的吗 或者 我是否需要先安装 SQLit
  • 两个整数乘积的模

    我必须找到c c a b mod m a b c m 是 32 位整数 但 a b 可以超过 32 位 我正在尝试找出一种计算 c 的方法 而不使用 long 或任何 gt 32 位的数据类型 有任何想法吗 如果m是质数 事情可以简化吗 注
  • 为什么即使我的哈希码值相同,“==”也会返回 false

    我写了一个像这样的课程 public class HashCodeImpl public int hashCode return 1 public static void main String args TODO Auto generat
  • 如何调试“com.android.okhttp”

    在android kitkat中 URLConnection的实现已经被OkHttp取代 如何调试呢 OkHttp 位于此目录中 external okhttp android main java com squareup okhttp 当
  • Jframe 内有 2 个 Jdialogs 的 setModal 问题

    当我设置第一个选项时 我遇到了问题JDialog模态 第二个非模态 这是我正在尝试实现的功能 单击 测试对话框 按钮 一个JDialog有名字自定义对话框 主要的将会打开 如果单击 是 选项自定义对话框主 其他JDialog named 自
  • 从 MATLAB 调用 Java?

    我想要Matlab程序调用java文件 最好有一个例子 需要考虑三种情况 Java 内置库 也就是说 任何描述的here http docs oracle com javase 6 docs api 这些项目可以直接调用 例如 map ja
  • Mockito 使用 @Mock 时将 Null 值注入到 Spring bean 中?

    由于我是 Spring Test MVC 的新手 我不明白这个问题 我从以下代码中获取了http markchensblog blogspot in search label Spring http markchensblog blogsp
  • 将人类日期(当地时间 GMT)转​​换为日期

    我正在服务器上工作 服务器正在向我发送 GMT 本地日期的日期 例如Fri Jun 22 09 29 29 NPT 2018在字符串格式上 我将其转换为日期 如下所示 SimpleDateFormat simpleDateFormat ne
  • 在Java中运行bat文件并等待

    您可能会认为从 Java 启动 bat 文件是一项简单的任务 但事实并非如此 我有一个 bat 文件 它对从文本文件读取的值循环执行一些 sql 命令 它或多或少是这样的 FOR F x in CD listOfThings txt do
  • 如何将 HTML 链接放入电子邮件正文中?

    我有一个可以发送邮件的应用程序 用 Java 实现 我想在邮件中放置一个 HTML 链接 但该链接显示为普通字母 而不是 HTML 链接 我怎样才能将 HTML 链接放入字符串中 我需要特殊字符吗 太感谢了 Update 大家好你们好 感谢
  • 如何在JPanel中设置背景图片

    你好 我使用 JPanel 作为我的框架的容器 然后我真的想在我的面板中使用背景图片 我真的需要帮助 这是我到目前为止的代码 这是更新 请检查这里是我的代码 import java awt import javax swing import
  • 将 JScrollPane 添加到 JFrame

    我有一个关于向 Java 框架添加组件的问题 我有一个带有两个按钮的 JPanel 和一个添加了 JTable 的 JScrollPane 我想将这两个添加到 JFrame 中 我可以将 JPanel 添加到 JFrame 或将 JScro
  • Android S8+ 警告消息“不支持当前的显示尺寸设置,可能会出现意外行为”

    我在 Samsung S8 Android 7 中收到此警告消息 APP NAME 不支持当前的显示尺寸设置 可能会 行为出乎意料 它意味着什么以及如何删除它 谢谢 通过添加解决supports screens 机器人 xlargeScre
  • java XMLSerializer 避免复杂的空元素

    我有这个代码 DocumentBuilderFactory factory DocumentBuilderFactory newInstance DocumentBuilder builder factory newDocumentBuil
  • Java 正则表达式中的逻辑 AND

    是否可以在 Java Regex 中实现逻辑 AND 如果答案是肯定的 那么如何实现呢 正则表达式中的逻辑 AND 由一系列堆叠的先行断言组成 例如 foo bar glarch 将匹配包含所有三个 foo bar 和 glarch 的任何
  • Log4j2 ThreadContext 映射不适用于parallelStream()

    我有以下示例代码 public class Test static System setProperty isThreadContextMapInheritable true private static final Logger LOGG
  • 抛出 Java 异常时是否会生成堆栈跟踪?

    这是假设我们不调用 printstacktrace 方法 只是抛出和捕获 我们正在考虑这样做是为了解决一些性能瓶颈 不 堆栈跟踪是在构造异常对象时生成的 而不是在抛出异常对象时生成的 Throwable 构造函数调用 fillInStack
  • Java 11 - 将 Spring @PostConstruct 替换为 afterPropertiesSet 或使用 initMethod

    我正在使用 spring 应用程序 有时会使用 PostConstruct用于代码和测试中的设置 看来注释将被排除在外Java 11 https www baeldung com spring postconstruct predestro
  • 由 Servlet 容器提供服务的 WebSocket

    上周我研究了 WebSockets 并对如何使用 Java Servlet API 实现服务器端进行了一些思考 我没有花费太多时间 但在使用 Tomcat 进行一些测试时遇到了以下问题 如果不修补容器或至少对 HttpServletResp
  • java'assert'和'if(){}else exit;'之间的区别

    java和java有什么区别assert and if else exit 我可以用吗if else exit代替assert 也许有点谷歌 您应该记住的主要事情是 if else 语句应该用于程序流程控制 而assert 关键字应该仅用于

随机推荐

  • java 仓库管理_Java仓库管理系统(一)

    从小到大没有写日记的习惯 但本着互联网开放 共享的原则 并且马士兵老师曾说 当你学会一些技能的时候 看到别人正被你会的东西所困扰 你应该去帮助他 所以把仓库管理系统的详解记录一下 说的可能不那么专业 但基本都能听懂 本人编程起步 有错误请指
  • 常见手机快充协议介绍

    早在几年前 各厂家就在逐步推进手机的充电速度 随着QC PD等一系列的充电协议不断更新 各家手机厂家都有自己的快充解决方案 不仅功率不一样 手机间的充电协议与标准也变得越发复杂 不同品牌的手机快充名称也各有不同 所以我们需要先清楚我们的手机
  • 垃圾箱清空后数据恢复(亲测有效)

    辛辛苦苦写PPT写了两周 误删了还没发现 然后顺便把垃圾箱也清空了 wps还因为云空间已满不能同步上传 连个记录都没有 真的是差点气到心梗 经历了一个小时的百度 恢复了我的PPT 记录一下数据的恢复过程 无图 恢复的时候过于烦躁 忘记截图
  • STM32单片机IAP介绍

    1 什么是IAP 首先区分下两个概念 ISP和IAP ISP In System Programming 在系统中编程 通过芯片专用的串行编程接口对其内部的程序存储器进行擦写 IAP In Application Programming 在
  • Unity中UGUI的Text实现超链接点击的解决方案

    Unity实现超链接点击 功能简介 C 脚本 使用方法 Demo工程内截图 Demo地址 功能简介 1 同一个Text内可以实现多个不同字符区域的点击 2 适配了中文 英文 韩文 日文 阿拉伯语等 更多语种待测试 C 脚本 文件名 UITe
  • uni-app 微信小程序vendor.js 过大的处理方式和分包优化

    小程序工具提示vendor js过大 已经跳过es6向es5转换 这个转换问题本身不用理会 因为vendor js已经是es5的了 关于体积控制 参考如下 使用运行时代码压缩 HBuilderX创建的项目勾选运行 gt 运行到小程序模拟器
  • Shell脚本编写教程【二】——Shell 数组

    Shell脚本编写教程 二 Shell 数组 目录 https blog csdn net shn111 article details 131590488 参考教程 https www runoob com linux linux she
  • colmap利用已知的相机内外参数重建场景

    Colmap根据相机内外参数重建稀疏模型 COLMAP稀疏重建得图像内外参文件 colmap model converter input path output path output type TXT
  • android 蓝牙ble 133,java – Android蓝牙错误133

    我正在尝试连接到 Android上的蓝牙设备 我在onClientConnectionState处理程序中收到状态133 我并不总是得到这个错误 有时连接很好 我无法指出触发问题的原因 重新启动设备和我的repro应用程序后 我甚至立即使用
  • 对于长度为5位的一个01串,每一位都可能是0或1,一共有32种可能。(蓝桥杯)

    题描述 对于长度为5位的一个01串 每一位都可能是0或1 一共有32种可能 它们的前几个是 00000 00001 00010 00011 00100 请按从小到大的顺序输出这32种01串 输入格式 本试题没有输入 输出格式 输出32行 按
  • 基于光标读取xml stax入门

    StAX 概述 从一开始 Java API for XML Processing JAXP 就提供了两种方法来处理 XML 文档对象模型 DOM 方法是用标准的对象模型表示 XML 文档 Simple API for XML SAX 方法使
  • 基于实时迭代的数值鲁棒NMPC双模稳定预测模型(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 Matlab代码实现 4 参考文献 1 概述 本文 T 秒进行 N 次模拟 使用提出的使
  • 计算机二级小蒋在教务处负责学生成绩,小蒋是一位中学教师,在教务处负责初一年级学生的成绩管理。由于学校地处偏远地区..._考试资料网...

    小蒋是一位中学教师 在教务处负责初一年级学生的成绩管理 由于学校地处偏远地区 缺 乏必要的教学设施 只有一台配置不太高的PC可以使用 他在这台电脑中安装了Microsoft Office 决定通过Excel来管理学生成绩 以弥补学校缺少数据
  • 电子企业应该先实施ERP系统还是WMS仓储管理系统

    电子企业应该先实施ERP系统还是WMS仓储管理系统 这是一个有争议的问题 不同的企业和管理专家有不同的看法 但是 从我个人的观点来看 电子企业应该先实施ERP系统 然后再考虑WMS仓储管理系统 首先 ERP系统是企业资源规划 Enterpr
  • React Native: 添加外部字体

    iOS 1 字体文件加入到Xcode项目中 2 确认字体文件包含在目标中 点击字体文件 查看是否在右栏的 Target Membership中选择了改字体 3 检查字体是否在打包的资源中 在Build Phases中查看字体是否添加在Cop
  • c++ point类(含输入和输出的重载)

    问题描述 定义类point 其中包括两个数据成员 均为 int 类型 为点的横坐标和纵坐标 类的成员函数如下 构造函数 包括两个参数 其两个参数的默认值为0 重载运算符 两个点相应的坐标相加 比如 1 1 2 2 3 3 两个点相应的坐标相
  • 华为OD机试真题- 目录删除-2023年OD统一考试(B卷)

    题目描述 某文件系统中有N个目录 每个目录都一个独一无二的ID 每个目录只有一个父目录 但每个父目录下可以有零个或者多个子目录 目录结构呈树状结构 假设 根目录的ID为0 且根目录没有父目录 其他所有目录的ID用唯一的正整数表示 并统一编号
  • 升级需谨慎,开发两行泪!——记一次MySQL驱动包升级引发的事故

    一 背景 最近项目组在版本迭代时 组件也要进行升级 此时涉及到MySQL驱动包jdbc的版本升级 即从5 1 X升级到8 0 X 然鹅在上线之后就出现了一部分兼容性问题 造成了一次 事故 调用接口出现 系统错误 查看日志 java time
  • Idea中Springboot开启热部署方法

    Springboot1 3后支持热部署 具体方法如下 1 增加依赖
  • 【Java学习笔记】70:借助Redis实现分布式锁

    这节学习Java用Redis做分布式锁 来做秒杀场景卖货减库存的案例 最原始的减库存写法 这里库存也存Redis里面 调减库存接口的时候判断一下大于0 还有库存 就拿出来减1 这里StringRedisTemplate是Spring Boo