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…...
终极Mac菜单栏整理神器:Ice让你的macOS界面瞬间清爽高效!
终极Mac菜单栏整理神器:Ice让你的macOS界面瞬间清爽高效! 【免费下载链接】Ice Powerful menu bar manager for macOS 项目地址: https://gitcode.com/GitHub_Trending/ice/Ice 还在为Mac顶部菜单栏拥挤不堪而烦恼吗?每次找图标都要眯…...
犀牛开发者日记-犀牛论剑特辑 | 李江浩:一个 ROS 布道师的转身
熟悉ROS领域的朋友,对李江浩这个名字想必并不陌生。作为资深ROS布道师,他常年活跃在技术社区分享干货,面对同行提出的各类问题,总能给出快准狠的解决方案,精准直击技术痛点。熟悉他的人都有一个共同感受:李…...
1951-2025年中国1km月平均气温逐年年内季节波动幅度数据集
中国1000米分辨率月平均气温数据集(1951-2025)提供了长时间序列、规则网格的气象背景信息,为开展气候变化分析和区域比较研究提供了基础数据支撑。针对原始月尺度序列直接使用不够便捷的问题,需要进一步形成具有明确主题和统一格式…...
Arcgis新手必看:用‘焦点统计’和‘设为空函数’搞定栅格数据清洗(附避坑要点)
ArcGIS栅格数据清洗实战:焦点统计与设为空函数的高效应用指南 当你第一次拿到一份满是噪点的DEM数据或存在异常值的土地利用分类图时,那种手足无措的感觉我深有体会。栅格数据清洗是GIS分析中看似简单却暗藏玄机的关键步骤,一个不当的参数设置…...
从Demo到实战:手把手教你用OpenMMLab的MMDetection训练自己的第一个目标检测模型(附数据集制作)
从零构建目标检测模型:OpenMMLab实战指南与数据集制作全流程 当你第一次成功运行OpenMMLab的Demo时,那种成就感可能很快会被新的困惑取代——如何让这套强大的工具识别你自己的数据?本文将带你跨越从"跑通示例"到"训练自定义模…...
SteamAutoCrack终极指南:5步掌握游戏DRM自动移除技术
SteamAutoCrack终极指南:5步掌握游戏DRM自动移除技术 【免费下载链接】Steam-auto-crack Steam Game Automatic Cracker 项目地址: https://gitcode.com/gh_mirrors/st/Steam-auto-crack 你是否曾为Steam游戏的DRM保护而烦恼?每次运行游戏都需要启…...
扩散模型在机器人控制中的多模态优化应用
1. 扩散模型在近似模型预测控制中的创新应用在机器人控制领域,模型预测控制(MPC)因其优秀的约束处理能力和优化性能而广受青睐。然而,传统MPC需要在线求解优化问题,计算成本高昂,难以满足高速实时控制的需求…...
思源宋体TTF字体包:为什么专业设计师都选择它?7大应用场景深度解析
思源宋体TTF字体包:为什么专业设计师都选择它?7大应用场景深度解析 【免费下载链接】source-han-serif-ttf Source Han Serif TTF 项目地址: https://gitcode.com/gh_mirrors/so/source-han-serif-ttf 还在为中文排版烦恼吗?字体选择困…...
CircuitPython内存优化与PyCharm集成:嵌入式开发实战指南
1. 项目概述与核心挑战在嵌入式开发的世界里,CircuitPython以其极低的入门门槛和强大的硬件抽象能力,成为了连接创意与现实的桥梁。无论是驱动一串炫彩的NeoPixel灯带,还是读取传感器数据,CircuitPython都让这一切变得像在桌面Pyt…...
在 Elasticsearch 中使用带有确定性护栏的 Agentic AI 搜索,以实现安全的查询执行
作者:来自 Elastic Alexander Marquardt, Honza Krl 及 Taylor Roy 当 LLM 直接生成查询时, Agentic AI 搜索系统通常会失败。了解确定性护栏和控制平面架构如何通过 Elasticsearch 实现安全、可靠且受治理的查询执行。 刚接触 Elasticsearch࿱…...
