Redis分布式锁进阶源码分析
Redis分布式锁进阶源码分析
- 1、如何写一个商品秒杀代码?
- 2、加上Java锁
- 3、使用redis setnx命令获取锁
- 4、增加try和finally
- 5、给锁设置过期时间
- 6、增长过期时间,并setnx增加唯一value
- 7、使用redisson
- 8、源码分析
- a、RedissonLock.tryLockInnerAsync
- b、RedissonLock.tryAcquireAsync
- 9、Redisson分布式锁的源码分析总结
根据秒杀场景演示
1、如何写一个商品秒杀代码?
@Autowired
StringRedisTemplate redisTemplate;public String stock() {String key = "stock_01";int stockNum = Integer.parseInt(redisTemplate.opsForValue().get(key));if (stockNum > 0) {redisTemplate.opsForValue().set(key, (stockNum - 1) + "");}else {return "fail";}return "success";
}
上面的写法会造成并发问题,多个客户端同时请求此方法,查询到的库存一致,同时扣减,导致超卖。
2、加上Java锁
public synchronized String stock() {String key = "stock_01";int stockNum = Integer.parseInt(redisTemplate.opsForValue().get(key));if (stockNum > 0) {redisTemplate.opsForValue().set(key, (stockNum - 1) + "");}else {return "fail";}return "success";
}
加上Java锁,会避免此问题,但是,如果是分布式项目,一个节点会部署到多个容器或者在多个Tomcat中运行,Java锁无法解决这种问题
3、使用redis setnx命令获取锁
每次执行扣减库存前,先用setnx命令插入一个标志,标记此线程方法获取到锁,获取成功方能扣减,不成功就返回。执行完扣减后删除标志。
注意:命令setnx key value,将 key 的值设为value,当且仅当key不存在;若给定的key已经存在,则不做任何动作。设置成功,返回1;设置失败,返回0。
public String stock() {String key = "stock_01";Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(key, "stock"); //setnxif(!ifAbsent){return "fail";}int stockNum = Integer.parseInt(redisTemplate.opsForValue().get(key));if (stockNum > 0) {redisTemplate.opsForValue().set(key, (stockNum - 1) + "");} else {return "fail";}redisTemplate.delete(key); //执行完扣减后删除keyreturn "success";
}
上面的代码如果执行完setnx命令后,程序异常报错,锁得不到释放,其他线程无法扣减库存,这时候就有人说了,可以加上try和finally,在finally中删除key这样就可以解决。
4、增加try和finally
public String stock() {String key = "stock_01";Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(key, "stock");try {if(!ifAbsent){return "fail";}int stockNum = Integer.parseInt(redisTemplate.opsForValue().get(key));if (stockNum > 0) {redisTemplate.opsForValue().set(key, (stockNum - 1) + "");} else {return "fail";}}finally {redisTemplate.delete(key); //执行完扣减后删除key}return "success";
}
如果执行到try中的代码服务器刚好宕机,没有执行finally中的删除key,还是不会释放锁,如何解决?
5、给锁设置过期时间
public String stock() {String key = "stock_01";Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(key, "stock",10, TimeUnit.SECONDS);//执行setnx,并给key设置过期时间10秒try {if(!ifAbsent){return "fail";}int stockNum = Integer.parseInt(redisTemplate.opsForValue().get(key));if (stockNum > 0) {redisTemplate.opsForValue().set(key, (stockNum - 1) + "");} else {return "fail";}}finally {redisTemplate.delete(key); //执行完扣减后删除key}return "success";
}
上面代码还是会有问题,如果扣减代码执行时间大于我们设置的过期时间,redis已经删除了key,其他线程可以获取到锁,并正常执行,但是第一次获取到锁的线程扣减完库存之后,执行了删除key的操作,导致下一个线程丢失锁。可以给这个setnx命令的value设置一个唯一值来区分哪个线程获取到锁
6、增长过期时间,并setnx增加唯一value
public String stock() {String key = "stock_01";String id = UUID.randomUUID().toString();//增加唯一id,Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(key, id, 30, TimeUnit.SECONDS);//把id存入到value中try {if (!ifAbsent) {return "fail";}int stockNum = Integer.parseInt(redisTemplate.opsForValue().get(key));if (stockNum > 0) {redisTemplate.opsForValue().set(key, (stockNum - 1) + "");} else {return "fail";}} finally {if (id.equals(redisTemplate.opsForValue().get(key))) {//对比id是否一致,一致才可删除锁,避免锁误删redisTemplate.delete(key); //执行完扣减后删除key}}return "success";
}
这时候已经能解决大部分秒杀场景了,虽然已经考虑的足够多的情况了,但是很不幸,上面代码还是会出现问题
a、增长过期时间其实治标不治本,出问题的概率会变小,但是不代表不会出问题,代码执行时间还是会超过过期时间,导致锁丢失
b、执行到finally中的对比id已经执行,而删除key没有执行,过期时间到了,此时第二个线程获取到锁,但是第一个线程又执行了删除,极端情况还是会出现误删锁导致超卖
面临这两个问题如何解决:
a、动态修改时间,即锁续命:开启一个线程执行一个定时任务,去判断获取锁的线程有没有结束,如果没结束就增加过期时间“续命”
b、判断有没有key和删除key的操作要有原子性:Java中没有提供这种操作,但是Lua脚本可以实现
7、使用redisson
a、引入pom:
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>2.7.0</version>
</dependency>
b、增加配置类:
@Configuration
@Slf4j
public class RedissonManager { //集群环境使用-节点信息 @Value("${spring.redis.cluster.nodes:default}")private String clusterNodes; //公共-密码 @Value("${spring.redis.password:default}")private String password;//单机环境使用@Value("${spring.redis.host:default}")private String host;//单机环境使用@Value("${spring.redis.port:6379}")private String port;//单机环境使用@Value("${spring.redis.database:0}")private int database;@Bean@ConditionalOnProperty(name = "spring.redis.mode", havingValue = "cluster")public RedissonClient redissonClient() {// 集群环境使用Config config = new Config();config.useClusterServers().addNodeAddress(clusterNodes.split(",")).setPassword(password);return Redisson.create(config);}@Bean@ConditionalOnProperty(name = "spring.redis.mode", havingValue = "singleton", matchIfMissing = true)public RedissonClient redissonSingletonClient() {// 单机打包使用Config config = new Config();config.useSingleServer().setAddress(host + ":" + port).setPassword(password).setDatabase(database);return Redisson.create(config);}
}
c、代码如下
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
Redisson redisson;public String stock() {String key = "stock_01";RLock lock = redisson.getLock(key);lock.lock();try {int stockNum = Integer.parseInt(redisTemplate.opsForValue().get(key));if (stockNum > 0) {redisTemplate.opsForValue().set(key, (stockNum - 1) + "");} else {return "fail";}} finally {lock.unlock();return "fail";}return "success";
}
8、源码分析
a、RedissonLock.tryLockInnerAsync
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {//锁续命的执行周期,默认30秒,this.internalLockLeaseTime = java.util.concurrent.TimeUnit.SECONDS.toMillis(30L);this.internalLockLeaseTime = unit.toMillis(leaseTime);//执行Lua脚本return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command,//redis.call,用于执行Redis脚本。这个命令会将脚本中的Redis命令调用转化为Lua数据类型,并执行这个脚本。//redis.call('exists', key),用于检查指定的键是否存在,如果键存在,则返回1;键不存在,则返回0。"if (redis.call('exists', KEYS[1]) == 0) then " +//判断key不存在// 保存到Hash(哈希表) 中// hset:指定要执行的Redis命令为hset,hset key field value:将哈希表key中的域field的值设为value// KEYS[1]:哈希表的键名,为this.getName()也就是代码中传过来的key// ARGV[2]:指定要设置的字段名,为this.getLockName(threadId),也就是value为当前线程id// 1:指定要将字段设置为的值"redis.call('hset', KEYS[1], ARGV[2], 1); " +// 设置过期时间// ARGV[1]为this.internalLockLeaseTime,默认30秒"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) " +//如果key存在,锁重入// hincrby:指定要执行的Redis命令为hincrby,hincrby key field increment:为哈希表key中的域field的值加上增量increment"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +// 重置过期时间为30秒"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +//以毫秒为单位返回key的剩余时间"return redis.call('pttl', KEYS[1]);",Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
}
b、RedissonLock.tryAcquireAsync
//此方法异步地尝试获取锁,它不会阻塞锁的线程
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {if (leaseTime != -1L) {//没有获取到锁,返回失败return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {//获取到锁RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(30L, TimeUnit.SECONDS, threadId, RedisCommands.EVAL_LONG);//注册一个回调方法,这个方法在异步方法执行完成执行ttlRemainingFuture.addListener(new FutureListener<Long>() {public void operationComplete(Future<Long> future) throws Exception {if (future.isSuccess()) {//执行完成获取结果Long ttlRemaining = (Long)future.getNow();if (ttlRemaining == null) {//scheduleExpirationRenewal会每隔10秒给锁刷新过期时间,默认置为30秒,直到这个锁获取不到RedissonLock.this.scheduleExpirationRenewal(threadId);}}}});return ttlRemainingFuture;}
}
9、Redisson分布式锁的源码分析总结
- 锁标识:Redisson使用Hash数据结构来表示锁。在这个Hash中,key为锁的名字,field为当前竞争锁成功的线程的唯一标识,value为重入次数。
- 队列:所有竞争锁失败的线程,会被放入一个队列中,等待锁的释放。这些线程会订阅当前锁的解锁事件,一旦锁被释放,就会唤醒队列中的一个线程来尝试获取锁。这个机制是通过Semaphore来实现的线程的挂起和唤醒。
- 加锁:加锁的核心源码在tryLockInnerAsync方法中。这个方法首先会将锁的租约时间转换为毫秒,然后执行一个Lua脚本尝试获取锁。如果获取锁成功,就会设置一个定时任务来续期锁的租约时间,避免锁因为超时而被自动释放。如果获取锁失败,就会将当前线程放入等待队列中,等待锁的释放。
- 解锁:解锁的核心源码在unlockInnerAsync方法中。这个方法会执行一个Lua脚本来释放锁。如果释放锁成功,就会唤醒等待队列中的一个线程来尝试获取锁。
Redisson分布式锁的实现原理主要基于Redis的单线程特性和Lua脚本的原子性。通过使用Lua脚本,可以保证加锁和解锁的操作是原子的,不会被其他操作打断。同时,通过定时任务来续期锁的租约时间,可以避免因为网络延迟等原因导致锁被提前释放。
总的来说,Redisson分布式锁的实现提供了一种高效、可靠的分布式锁解决方案,可以很好地满足分布式系统中的并发控制需求。
相关文章:
Redis分布式锁进阶源码分析
Redis分布式锁进阶源码分析 1、如何写一个商品秒杀代码?2、加上Java锁3、使用redis setnx命令获取锁4、增加try和finally5、给锁设置过期时间6、增长过期时间,并setnx增加唯一value7、使用redisson8、源码分析a、RedissonLock.tryLockInnerAsyncb、Redis…...

lag-llama源码解读(Lag-Llama: Towards Foundation Models for Time Series Forecasting)
Lag-Llama: Towards Foundation Models for Time Series Forecasting 文章内容: 时间序列预测任务,单变量预测单变量,基于Llama大模型,在zero-shot场景下模型表现优异。创新点,引入滞后特征作为协变量来进行预测。 获得…...

Three.js基础入门介绍——Three.js学习三【借助控制器操作相机】
在Three.js基础入门介绍——Three.js学习二【极简入门】中介绍了如何搭建Three.js开发环境并实现一个包含旋转立方体的场景示例,以此为前提,本篇将引进一个控制器的概念并使用”轨道控制器”(OrbitControls)来达到从不同方向展示场…...

【日志系列】什么是分布式日志系统?
✔️什么是分布式日志系统? 现在,很多应用都是集群部署的,一次请求会因为负载均衡而被路由到不同的服务器上面,这就导致一个应用的日志会分散在不同的服务器上面。 当我们要向通过日志做数据分析,问题排查的时候&#…...

[卷积神经网络]FCOS--仅使用卷积的Anchor Free目标检测
项目源码: FCOShttps://github.com/tianzhi0549/FCOS/ 一、概述 作为一种Anchor Free的目标检测网络,FCOS并不依赖锚框,这点类似于YOLOx和CenterNet,但CenterNet的思路是寻找目标的中心点,而FCOS则是寻找每个像素点&…...

Ubuntu fcitx Install
ubuntu经常出现键盘失灵的问题 查询资料得知应该是Ibus框架的问题 于是需要安装fcitx框架和搜狗拼音 sudo apt update sudo apt install fcitx 设置fcitx开机自启动(建议) sudo cp /usr/share/applications/fcitx.desktop /etc/xdg/autostart/ 然后…...
【Makefile/GNU Make】知识总结
文章目录 1. 总体认识2. 编写Makefile2.1. Makefile的组成2.2. Makefile文件名2.3. 包含其他Makefile 3. 编写规则4. 编写规则中的构建命令5. 如何使用变量6. 条件判断7. 转换文本的函数8. 如何运行make9. 使用模糊规则10. 使用make来更新存档文件11. 扩展GNU make12. 集成GNU …...

腾讯云轻量服务器和云服务器CVM该怎么选?区别一览
腾讯云轻量服务器和云服务器CVM该怎么选?不差钱选云服务器CVM,追求性价比选择轻量应用服务器,轻量真优惠呀,活动 https://curl.qcloud.com/oRMoSucP 轻量应用服务器2核2G3M价格62元一年、2核2G4M价格118元一年,540元三…...

MySQL定时备份实现
一、备份数据库 –all-databases 备份所有数据库 /opt/mysqlcopy/all_$(date “%Y-%m-%d %H:%M:%S”).sql 备份地址 docker exec -it 容器名称 sh -c "mysqldump -u root -ppassword --all-databases > /opt/mysqlcopy/all_$(date "%Y-%m-%d %H:%M:%S").sq…...

Nginx 不同源Https请求Http 报strict-origin-when-cross-origin
原因: nginx代理配置url指向只开放了/* 而我/*/*多了一层路径 成功:...

openGauss学习笔记-175 openGauss 数据库运维-备份与恢复-导入数据-管理并发写入操作示例
文章目录 openGauss学习笔记-175 openGauss 数据库运维-备份与恢复-导入数据-管理并发写入操作示例175.1 相同表的INSERT和DELETE并发175.2 相同表的并发INSERT175.3 相同表的并发UPDATE175.4 数据导入和查询的并发 openGauss学习笔记-175 openGauss 数据库运维-备份与恢复-导入…...
pnpm、npm、yarn是什么?怎么选择?
pnpm、npm、yarn三者是前端常用的包管理器,那么他们有什么区别呢? 1. npm (Node Package Manager) npm是Node.js的默认包管理器。自Node.js发布以来,npm就一直作为它的一个组成部分存在,因此,安装Node.js时也会自动安…...
MySQL8 一键部署
#!/bin/bash ### 定义变量 mysql_download_urlhttps://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.33-linux-glibc2.12-x86_64.tar.xz mysql_package_namemysql-8.0.33-linux-glibc2.12-x86_64.tar.xz mysql_dec_namemysql-8.0.33-linux-glibc2.12-x86_64 mysql_download_…...

12 UVM Driver
目录 12.1 uvm_driver class hierarchy 12.2 How to write driver code? 12.3 UVM Driver example 12.4 How to get sequence items from the sequencer? 12.5 UVM driver methods 12.5.1 Using get_next_item/ try_next_item and item_done methods 12.5.2 Using get…...
“暂存”校验逻辑探讨
1、背景 在业务中可能会遇到这种场景,前端页面元素多且复杂,一次性填完提交耗时很长,中间中断面临着丢失数据的风险。针对这个问题,“暂存”应运而生。 那“暂存”的时候,是否需要对数据校验,如何进行校验…...

探究element-ui 2.15.8中<el-input>的keydown事件无效问题
一、问题描述 今天看到一个问题,在用Vue2element-ui 2.15.8开发时,使用input组件绑定keydown事件没有任何效果。 <template><div id"app"><el-input v-model"content" placeholder"请输入" keydown&quo…...

Unity 代码控制Text自适应文本高度
在使用代码给Text赋值时,且文本有多段,并需要根据实际文本高度适配Text组件的高度时,可以使用以下方法: //Text文本 public TextMeshProUGUI text;void Start() {//代码赋值文本text.text "好!\n很好!\n非常好!";//获…...

TiDB 7.1 多租户在中泰证券中的应用
本文详细介绍了中泰证券在系统国产化改造项目中采用 TiDB 多租户技术的实施过程。文章分析了中泰证券数据库系统现状以及引入 TiDB 资源管控技术的必要性,探讨了 TiDB 多租户的关键特性,并阐述了在实际应用中的具体操作步骤。通过该技术的应用࿰…...

嵌入式-stm32-SR04超声波测距介绍及实战
一:超声波传感器介绍 1.1、SR04超声波测距硬件模块 1.2、SR04的四个IO口 vcc:提供电源5V gnd:接地 Trig:是**发送**声波信号的触发器 Echo:是**接收**回波信号的引脚 当TRIG信号被触发时,传感器会发送一定频率的声波信号,该信号被反射后&am…...

智能优化算法应用:基于白鲸算法3D无线传感器网络(WSN)覆盖优化 - 附代码
智能优化算法应用:基于白鲸算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用:基于白鲸算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.白鲸算法4.实验参数设定5.算法结果6.参考文献7.MA…...

CMake基础:构建流程详解
目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...

WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成
厌倦手动写WordPress文章?AI自动生成,效率提升10倍! 支持多语言、自动配图、定时发布,让内容创作更轻松! AI内容生成 → 不想每天写文章?AI一键生成高质量内容!多语言支持 → 跨境电商必备&am…...

MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...
OpenLayers 分屏对比(地图联动)
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 地图分屏对比在WebGIS开发中是很常见的功能,和卷帘图层不一样的是,分屏对比是在各个地图中添加相同或者不同的图层进行对比查看。…...
LeetCode - 199. 二叉树的右视图
题目 199. 二叉树的右视图 - 力扣(LeetCode) 思路 右视图是指从树的右侧看,对于每一层,只能看到该层最右边的节点。实现思路是: 使用深度优先搜索(DFS)按照"根-右-左"的顺序遍历树记录每个节点的深度对于…...

【网络安全】开源系统getshell漏洞挖掘
审计过程: 在入口文件admin/index.php中: 用户可以通过m,c,a等参数控制加载的文件和方法,在app/system/entrance.php中存在重点代码: 当M_TYPE system并且M_MODULE include时,会设置常量PATH_OWN_FILE为PATH_APP.M_T…...

手机平板能效生态设计指令EU 2023/1670标准解读
手机平板能效生态设计指令EU 2023/1670标准解读 以下是针对欧盟《手机和平板电脑生态设计法规》(EU) 2023/1670 的核心解读,综合法规核心要求、最新修正及企业合规要点: 一、法规背景与目标 生效与强制时间 发布于2023年8月31日(OJ公报&…...

关于easyexcel动态下拉选问题处理
前些日子突然碰到一个问题,说是客户的导入文件模版想支持部分导入内容的下拉选,于是我就找了easyexcel官网寻找解决方案,并没有找到合适的方案,没办法只能自己动手并分享出来,针对Java生成Excel下拉菜单时因选项过多导…...

使用SSE解决获取状态不一致问题
使用SSE解决获取状态不一致问题 1. 问题描述2. SSE介绍2.1 SSE 的工作原理2.2 SSE 的事件格式规范2.3 SSE与其他技术对比2.4 SSE 的优缺点 3. 实战代码 1. 问题描述 目前做的一个功能是上传多个文件,这个上传文件是整体功能的一部分,文件在上传的过程中…...

华为云Flexus+DeepSeek征文 | 基于Dify构建具备联网搜索能力的知识库问答助手
华为云FlexusDeepSeek征文 | 基于Dify构建具备联网搜索能力的知识库问答助手 一、构建知识库问答助手引言二、构建知识库问答助手环境2.1 基于FlexusX实例的Dify平台2.2 基于MaaS的模型API商用服务 三、构建知识库问答助手实战3.1 配置Dify环境3.2 创建知识库问答助手3.3 使用知…...