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

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、如何写一个商品秒杀代码&#xff1f;2、加上Java锁3、使用redis setnx命令获取锁4、增加try和finally5、给锁设置过期时间6、增长过期时间&#xff0c;并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 文章内容&#xff1a; 时间序列预测任务&#xff0c;单变量预测单变量&#xff0c;基于Llama大模型&#xff0c;在zero-shot场景下模型表现优异。创新点&#xff0c;引入滞后特征作为协变量来进行预测。 获得…...

Three.js基础入门介绍——Three.js学习三【借助控制器操作相机】

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

【日志系列】什么是分布式日志系统?

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

[卷积神经网络]FCOS--仅使用卷积的Anchor Free目标检测

项目源码&#xff1a; FCOShttps://github.com/tianzhi0549/FCOS/ 一、概述 作为一种Anchor Free的目标检测网络&#xff0c;FCOS并不依赖锚框&#xff0c;这点类似于YOLOx和CenterNet&#xff0c;但CenterNet的思路是寻找目标的中心点&#xff0c;而FCOS则是寻找每个像素点&…...

Ubuntu fcitx Install

ubuntu经常出现键盘失灵的问题 查询资料得知应该是Ibus框架的问题 于是需要安装fcitx框架和搜狗拼音 sudo apt update sudo apt install fcitx 设置fcitx开机自启动&#xff08;建议&#xff09; 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该怎么选&#xff1f;不差钱选云服务器CVM&#xff0c;追求性价比选择轻量应用服务器&#xff0c;轻量真优惠呀&#xff0c;活动 https://curl.qcloud.com/oRMoSucP 轻量应用服务器2核2G3M价格62元一年、2核2G4M价格118元一年&#xff0c;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

原因&#xff1a; nginx代理配置url指向只开放了/* 而我/*/*多了一层路径 成功&#xff1a;...

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三者是前端常用的包管理器&#xff0c;那么他们有什么区别呢&#xff1f; 1. npm (Node Package Manager) npm是Node.js的默认包管理器。自Node.js发布以来&#xff0c;npm就一直作为它的一个组成部分存在&#xff0c;因此&#xff0c;安装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、背景 在业务中可能会遇到这种场景&#xff0c;前端页面元素多且复杂&#xff0c;一次性填完提交耗时很长&#xff0c;中间中断面临着丢失数据的风险。针对这个问题&#xff0c;“暂存”应运而生。 那“暂存”的时候&#xff0c;是否需要对数据校验&#xff0c;如何进行校验…...

探究element-ui 2.15.8中<el-input>的keydown事件无效问题

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

Unity 代码控制Text自适应文本高度

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

TiDB 7.1 多租户在中泰证券中的应用

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

嵌入式-stm32-SR04超声波测距介绍及实战

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

智能优化算法应用:基于白鲸算法3D无线传感器网络(WSN)覆盖优化 - 附代码

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

使用VSCode开发Django指南

使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架&#xff0c;专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用&#xff0c;其中包含三个使用通用基本模板的页面。在此…...

React Native 开发环境搭建(全平台详解)

React Native 开发环境搭建&#xff08;全平台详解&#xff09; 在开始使用 React Native 开发移动应用之前&#xff0c;正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南&#xff0c;涵盖 macOS 和 Windows 平台的配置步骤&#xff0c;如何在 Android 和 iOS…...

【位运算】消失的两个数字(hard)

消失的两个数字&#xff08;hard&#xff09; 题⽬描述&#xff1a;解法&#xff08;位运算&#xff09;&#xff1a;Java 算法代码&#xff1a;更简便代码 题⽬链接&#xff1a;⾯试题 17.19. 消失的两个数字 题⽬描述&#xff1a; 给定⼀个数组&#xff0c;包含从 1 到 N 所有…...

Golang dig框架与GraphQL的完美结合

将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用&#xff0c;可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器&#xff0c;能够帮助开发者更好地管理复杂的依赖关系&#xff0c;而 GraphQL 则是一种用于 API 的查询语言&#xff0c;能够提…...

人机融合智能 | “人智交互”跨学科新领域

本文系统地提出基于“以人为中心AI(HCAI)”理念的人-人工智能交互(人智交互)这一跨学科新领域及框架,定义人智交互领域的理念、基本理论和关键问题、方法、开发流程和参与团队等,阐述提出人智交互新领域的意义。然后,提出人智交互研究的三种新范式取向以及它们的意义。最后,总结…...

基于Springboot+Vue的办公管理系统

角色&#xff1a; 管理员、员工 技术&#xff1a; 后端: SpringBoot, Vue2, MySQL, Mybatis-Plus 前端: Vue2, Element-UI, Axios, Echarts, Vue-Router 核心功能&#xff1a; 该办公管理系统是一个综合性的企业内部管理平台&#xff0c;旨在提升企业运营效率和员工管理水…...

[ACTF2020 新生赛]Include 1(php://filter伪协议)

题目 做法 启动靶机&#xff0c;点进去 点进去 查看URL&#xff0c;有 ?fileflag.php说明存在文件包含&#xff0c;原理是php://filter 协议 当它与包含函数结合时&#xff0c;php://filter流会被当作php文件执行。 用php://filter加编码&#xff0c;能让PHP把文件内容…...

6️⃣Go 语言中的哈希、加密与序列化:通往区块链世界的钥匙

Go 语言中的哈希、加密与序列化:通往区块链世界的钥匙 一、前言:离区块链还有多远? 区块链听起来可能遥不可及,似乎是只有密码学专家和资深工程师才能涉足的领域。但事实上,构建一个区块链的核心并不复杂,尤其当你已经掌握了一门系统编程语言,比如 Go。 要真正理解区…...

Windows电脑能装鸿蒙吗_Windows电脑体验鸿蒙电脑操作系统教程

鸿蒙电脑版操作系统来了&#xff0c;很多小伙伴想体验鸿蒙电脑版操作系统&#xff0c;可惜&#xff0c;鸿蒙系统并不支持你正在使用的传统的电脑来安装。不过可以通过可以使用华为官方提供的虚拟机&#xff0c;来体验大家心心念念的鸿蒙系统啦&#xff01;注意&#xff1a;虚拟…...

Python环境安装与虚拟环境配置详解

本文档旨在为Python开发者提供一站式的环境安装与虚拟环境配置指南&#xff0c;适用于Windows、macOS和Linux系统。无论你是初学者还是有经验的开发者&#xff0c;都能在此找到适合自己的环境搭建方法和常见问题的解决方案。 快速开始 一分钟快速安装与虚拟环境配置 # macOS/…...