详解Redis分布式锁在SpringBoot的@Async方法中没锁住的坑
背景
Redis分布式锁很有用处,在秒杀、抢购、订单、限流特别是一些用到异步分布式并行处理任务时频繁的用到,可以说它是一个BS架构的应用中最高频使用的技术之一。
但是我们经常会碰到这样的一个问题,那就是我们都按照标准做了但有时运行着、运行着就是没锁住的问题。
一旦出了这样的问题特别难调试以及排查,因为在异步并行的环境下计算机代码的执行是乱序的,而且有一个“概率”问题。往往测10次结果都是对的。此时测试团队以为这次交付没有问题了于是布署上线,而上了线后会产生:要么一次都不对或者前10次对的第11次就是不对的。
要知道,锁的问题出了事不是小事。一旦出事对用户来说就和“死机”一样,死活无法操作了,亦或者时操作的结果乱通知、乱扣钱、随机不能下单,此时后台唯有找到锁键值,然后人为的把这个键值给“剁”掉才能解决。
因此,今天就借着刚排查的2个生产问题我们把锁的机制彻底的了解一下。
Redis锁的正确使用方式
//使用RedissonClient锁@Autowired
private RedissonClient redissonSentinel;//申明锁
RLock lock = null;
lock = redissonSentinel.getLock(lockKey);if (lock != null && lock.isLocked()) {//已经有一个任务在进行了,因此不能执行此时系统需要根据业务逻辑或进入等待或者return
}
try{lock.tryLock(0, TimeUnit.SECONDS);// 用自续约锁上锁接着下面就是做某事了
}catch(Exception e){}finally { //直接按此解锁,永不出错,也不要在finally里去判断,而是强制在finally里关闭-大厂佳实践try {lock.unlock();} catch (Exception e) {}
}
以上是一个标准的Redis分布式锁的标准公式,下面给出配置
redis:password: 111111nodes: 192.1.0.11:7001redisson:nodes: redis://192.1.0.11:27001,redis://192.1.0.12:27001,redis://192.1.0.13:7001:27003sentinel: nodes: 192.1.0.11:27001,192.1.0.12:27001,192.1.0.13:27001master: master1subscriptionsPerConnection: 50 #分布式锁必设此参数可以考虑放大它占用redis连接subscriptionConnectionPoolSize: 200 #分布式锁必设此参数可以考虑放大它占用redis连接
千万不要忘了这两关键字,很多人不设的话那么会出现生产上订单、并发一多直接会抛出redis锁连接不够用的错:
- subscriptionsPerConnection
- subscriptionConnectionPoolSize
生产典型问题
下面我们就来看自以为锁住了但是在生产上随机的“飘”的问题,要么锁死要么就没锁住的具体案例来讲解Redis分布式锁的一些坑吧。
每个用户只可以有一个文件导出没但没锁住
具体场景
每个用户在一个数据展式面板里查看数据,看到了自己要的数据就可以选择1万条做导出,导出时用户可以关闭当前页面甚至退出,后台任务导完后会以消息形式通知到用户,用户在自己的个人头像上可以看到一个小红点闪出。
需求
根据需求,这是一个云上的SAAS应用,我们对普通用户只提供同时只可以有一个导出任务在后台运行的机制。
当后台己有一条任务正在导出时用户此时在数据面板里就算点几十次“导出”都因该提醒用户“当前您有一个导出任务正在进行中”。
实际有问题代码
我看了一下代码,还挺公整的,它是这么判断的。
//使用RedissonClient锁
@Service
public class ExportService{@Autowiredprivate RedissonClient redissonSentinel;@Asyncpublic void exportTask(){String redisLock=EXPORT_TASK_LOCK_KEY+":"+companyId+":"+loginId//申明锁RLock lock = null; lock = redissonSentinel.getLock(lockKey);if (lock != null && lock.isLocked()) {//已经有一个任务在进行了,因此不能执行此时系统需要根据业务逻辑或进入等待或者return}try{lock.tryLock(0, TimeUnit.SECONDS);// 用自续约锁上锁接着下面就是做某事了}catch(Exception e){}finally { //直接按此解锁,永不出错,也不要在finally里去判断,而是强制在finally里关闭-大厂佳实践try {lock.unlock();} catch (Exception e) {}}}
}
对锁认识上的误区
我一眼就看出了问题,但我没有声张,我让开发和他的Leader以及我们的架构师一起来看。我这么提出问题让他们自己开动脑筋去想这个问题。
1. 首先,我们看到这个锁用companyId+loginId的确是可以做到锁的这个key唯一;
2. 但实际是没锁住因为前端用户在一个任务没有导完后再点按钮有时可以并发出两条导出任务有时只能并发出一条这是事实,那么肯定不是这个key唯一的问题;
3. 我们一起打开redis客户端用命令来查看服务器在导出任务时锁产生的情况,的确是看到产生的这个锁的key对于不同的人是唯一的key;
我的问题是:锁的key是唯一的就一定会被锁住吗?
三个人搔搔头回答我:可能吧!
哈哈,问题就出在这。
都以为只要锁的key是唯一,这个key被锁住了那另一个操作带着同样的lock key进来获取到的状态就一定是“已经上锁”。
这个认知上错误了!!!
对于Redis分布式锁正确的认知
锁是存在于服务器上的,它不存在于客户端,同一个key来锁固然没错,但是我们看到了这个方法是一个被标为@Async的。
于是同一个客户点击一次就会生成一个Service类的exportTask进程。再点击一次又生成了一个Service类的exportTask进程。
当有10个exportTask进程时,我们虽然用的都是同一个lock key
String redisLock=EXPORT_TASK_LOCK_KEY+":"+companyId+":"+loginId
但是别忘了,这个Service方法的完整运行机制是怎么样的?
@Asyncpublic void exportTask(){String redisLock=EXPORT_TASK_LOCK_KEY+":"+companyId+":"+loginId//申明锁RLock lock = null; lock = redissonSentinel.getLock(lockKey);if (lock != null && lock.isLocked()) {//已经有一个任务在进行了,因此不能执行此时系统需要根据业务逻辑或进入等待或者return}try{lock.tryLock(0, TimeUnit.SECONDS);// 用自续约锁上锁
看到没?
每次都要RLock lock=null,再lock = redissonSentinel.getLock(lockKey)一下。
此时后台有10个exportTask,这10个exportTask彼此都在实例化自己的锁、锁语句。这下好玩了,因为是异步的,是乱序的,所以此时发生了这么一件肉眼不可见的事:
- exportTask1刚锁住正在操作导出还没有操作完时。
- exportTask2进程被创建时把这个锁的状态给置成“初始化状态了“。
- exportTask2于是在exportTask1还没有完成任务和释放锁时就又可以接着执行了。
这就是我们说的“没锁住”。
嘿嘿,这一切是@Async惹的祸。
如何改?
但我们又必须让这个方法是一个@Async的,因此怎么办?
把这个锁“上浮”到controller层。
在@RestController层的public ResponseBean exportDataAPI方法里如下申明
RLock uploadLock = null;uploadLock = exportService.canLock(companyId);if (uploadLock != null && !uploadLock.isLocked()) {message = "导出中";exportService.exportTask(ut, data, uploadLock);}else{logger.info(">>>>>>有一个任务已经在导出,当前步骤不执行")}
在Service方法中放入一个canLock方法如下
public RLock canLock(int companyId) {StringBuilder lockKeySB = new StringBuilder();lockKeySB.append(FoodTrainLLMConstants.LLM_UPLOAD_LOCK).append(companyId);RLock lock = redissonSentinel.getLock(lockKeySB.toString());return lock;}
然后我们在Service中如此改写原有逻辑
//使用RedissonClient锁
@Service
public class ExportService{@Autowiredprivate RedissonClient redissonSentinel;@Asyncpublic void exportTask(Rlock lock){try{lock.tryLock(0, TimeUnit.SECONDS);// 用自续约锁上锁接着下面就是做某事了}catch(Exception e){}finally { //直接按此解锁,永不出错,也不要在finally里去判断,而是强制在finally里关闭-大厂佳实践try {lock.unlock();} catch (Exception e) {}}}
}
这就是把锁“上浮”,让其真正把一整个“进程”给锁住,于是这个问题就可以被解决了。
小结
这种错误一般在于非@Async中就算一开始写成了错误的那种写法你也发现不了,这是因为一切都是同步的。
而一旦但有了@Async后,上述生产问题就发生了。
多线程中希望每个用户只可以存在一个查询任务但实际没有锁住
这是另一个场景,但是其也发生在一个@Async方法中。
即在一个@Asynce标注的方法中还有一个while,而要锁是锁的while中的步骤。
于是我们看到了这样的代码
@Async
public void backendQueryImageTask(String ut, int companyId, String loginId, String userInputPrompt, String taskId) throws Exception { while (System.currentTimeMillis() - startTime < timeoutMillis) {String lockKey = Query_Image_Redis_Lock_PREFIX + companyId + ":" + uid;RLock lock = null; lock = redissonSentinel.getLock(lockKey);if (lock != null && lock.isLocked()) {logger.info(">>>>>>当前是backendQueryImageTask,图片还在生成中另一个任务在查询中,当前任务不进行");continue;}try {lock.tryLock(0, TimeUnit.SECONDS);// 上锁
。。。。。。catch (Exception e) {logger.error("Error in backendQueryImageTask", e);return;} finally {try {lock.unlock();} catch (Exception e) {}}
实际代码问题
这个问题和第一个问题其实是一样,因为锁是运行在服务器端的,它的状态不是维持在客户端。因此当这个方法如果是@Asynce时代表着后台会存在若干进程,而我们这次的需求是在每一个进程里再有一个while,而在while中运行时必须锁住。
但实际没有锁住也正是因为每一次循环时另一个进程把同一把本己锁住正在执行任务的锁的状态给连续的做了这样的操作:
RLock lock=null - > redissonSentinel.getLock(lockKey);
这就破坏了还在上锁的服务器上的同一把锁的状态导致了这个锁失效。
如何改?
String lockKey = Query_Image_Redis_Lock_PREFIX + companyId + ":" + uid;RLock lock = null; lock = redissonSentinel.getLock(lockKey);while (System.currentTimeMillis() - startTime < timeoutMillis) {if (lock != null && lock.isLocked()) {logger.info(">>>>>>当前是backendQueryImageTask,图片还在生成中另一个任务在查询中,当前任务不进行");continue;}try {lock.tryLock(0, TimeUnit.SECONDS);// 上锁//do something} catch (Exception e) {logger.error("Error in backendQueryImageTask", e);return;} finally {try {lock.unlock();} catch (Exception e) {}}}
把锁的申明外置到while循环外即可成功达到我们的要求。
总结
锁一定是存在于服务器的,锁要锁的这个范围本身是异步运行的,因为如果是同步操作也没有这个锁的必要了。正因为是异步,所以服务器上的锁的对象是同一个,而当这个对象在异步并行时是乱序的,因此就会存在一个子进程“污染”到了另一个子进程里的锁对象。
为了成功把一组进程、业务原子方法锁住,这个锁的范围必须控制在其外层且这个锁的初始状态只可以被初始化一次。
此处我们考虑第一个例子中为什么把锁放在controller方法中这个锁就可以成功锁住Service里的方法呢?
这是因为每次用户在Controller方法中就算RLock lock=null时此时初始化的lock不是同一个对象,这是Controller方法的特性。
而只有当redissonSentinel.getLock(lockKey);时才会去拿服务端的锁,而此时这把锁的状态如果还没有被释放那么就一定是被锁住的。
附:redisson自续约锁的概念
当我们这样操作时
lock.tryLock(0, TimeUnit.SECONDS);// 上锁
很多人会习惯性的在参数里把这个0改成30或者60。这样做反而是画蛇添足、错误的做法。
因为加上了一个确切的数字后就会有问题!你怎么知道这个方法正好执行了30秒或者是60秒就一定完成了呢?如果这个方法是需要62秒怎么办?你不是把方法给打断了。
因此,Redisson特有的自续约锁就是把这个值设成0。于是在后台Redisson锁会先给到锁30秒时间。
当第20多秒还没有碰到有用户调用finally里的unlock时它会再给这个key延续30秒。。。再没执行完再给它30秒。
直到碰到finally块里被显示的调用了unlock,那么代表任务结束,这把锁的状态才会变成“释放”。
相关文章:

详解Redis分布式锁在SpringBoot的@Async方法中没锁住的坑
背景 Redis分布式锁很有用处,在秒杀、抢购、订单、限流特别是一些用到异步分布式并行处理任务时频繁的用到,可以说它是一个BS架构的应用中最高频使用的技术之一。 但是我们经常会碰到这样的一个问题,那就是我们都按照标准做了但有时运行着、…...

怎么做接口自动化测试
在分层测试的“金字塔”模型中,接口测试属于第二层服务集成测试范畴。相比UI层(主要是WEB或APP)自动化测试而言,接口自动化测试收益更大,且容易实现,维护成本低,有着更高的投入产出比࿰…...

网络编程(18)——使用asio协程实现并发服务器
十八、day18 到目前为止,我们以及学习了单线程同步/异步服务器、多线程IOServicePool和多线程IOThreadPool模型,今天学习如何通过asio协程实现并发服务器。 并发服务器有以下几种好处: 协程比线程更轻量,创建和销毁协程的开销较…...

Koa2项目实战2(路由管理、项目结构优化)
添加路由(处理不同的URL请求) 路由:根据不同的URL,调用对应的处理函数。 每一个接口服务,最核心的功能是:根据不同的URL请求,返回不同的数据。也就是调用不同的接口返回不同的数据。 在 Node…...

决战Linux操作系统
前言: 你是否也曾经为Linux所困扰过,在网上找的资料零零散散,是否学完Linux后还是懵懵懂懂,别怕,这篇博客是博主精心为你准备的,现在,就让我们一起来走进Linux的世界,决战Linux&…...

OceanBase 3.2.2 数据库问题处理记录
只记录OceanBase 数据库与OCP的异常处理,其它组件暂时不写录。 一、问题1: 说明:OMS 出现异常,无法访问(OB无法访问) OB数据库架构:1:1:1 原因:某一台OBserver因为内存问题,被服务器直接kill掉…...

HCIP--以太网交换安全(二)端口安全
端口安全 一、端口安全概述 1.1、端口安全概述:端口安全是一种网络设备防护措施,通过将接口学习的MAC地址设为安全地址防止非法用户通信。 1.2、端口安全原理: 类型 定义 特点 安全动态MAC地址 使能端口而未是能Stichy MAC功能是转换的…...

在 Windows 11 安卓子系统中安装 APK 的操作指南
这个软件好像不可以在纯android系统中使用(不知道是缺了什么),其他对于android的虚拟机要不缺少必要功能组件,要不性能过于低下。本方法致力于在带有谷歌框架WSA中运行该APK 在 Windows 11 安卓子系统中安装 APK 的操作指南 本指…...

[C语言] 函数详解:库函数与自定义函数
文章目录 函数的概念库函数和自定义函数库函数使用库函数示例常用库函数及头文件 自定义函数自定义函数的基本结构示例:实现两个数的求和函数自定义函数的好处 函数的返回值有返回值的函数无返回值的函数 函数的声明与调用声明函数在另一个文件中调用函数示例&#…...

0x11 科迈 RAS系统 Cookie验证越权漏洞
参考: 科迈 RAS系统 Cookie验证越权漏洞 | PeiQi文库 (wgpsec.org)免责声明 欢迎访问我的博客。以下内容仅供教育和信息用途: 合法性:我不支持或鼓励非法活动。请确保遵守法律法规。信息准确性:尽管我尽力提供准确的信息,但不保证其完全准确或适用。使用前请自行验证。风…...

MoonBit 双周报 Vol.57:AI助手功能增强、表达式优先级调整、JS 交互优化、标准库与实验库API多项更新!
2024-10-08 IDE更新 AI Codelens支持 /generate 和 /fix 命令 /generate 命令能够提供一个通用的用以生成代码的聊天界面。 /fix 命令能够读取当前函数的错误信息给出修复建议。 MoonBit更新 调整中缀表达式和if、match、loop、while、for、try表达式的优先级, 后者这些控制…...

element ui input textarea控制显示高度
样式代码 .testPage { position: absolute; left: 0; top: 0; right: 0; bottom: 0; display: flex; height: 100%; /* 控制输入框高度 */ .el-textarea { height: 90%; ::v-deep .el-textarea__inner { height: 90%; } } }...

Chromium 中chrome.downloads扩展接口c++
一、前端chrome.downloads 使用 chrome.downloads API 以编程方式启动、监控、操作和搜索下载内容。 权限 downloads 您必须在扩展程序清单中声明 "downloads" 权限,才能使用此 API。 {"name": "My extension",..."permiss…...

微信小程序常见问题
一、编译报错 [ app.json 文件内容错误] app.json: 在项目根目录未找到 app.json 解决办法: 微信开发者工具中打开设置->安全设置->打开服务端口用HBuilder X打开小程序文件夹,点击“运行到小程序模拟器”,生成配置文件,…...

进程的理解
进程的理解 目录: 什么是进程主要特征主要组成部分进程状态进程优先级 1.什么是进程 概念: 在操作系统中,**进程(Process)**是一个正在执行的程序实例。可以将进程理解为一个动态的实体,它不仅包括静态…...

LeetCode494:目标和
题目链接:494. 目标和 - 力扣(LeetCode) 代码如下 class Solution { public:int findTargetSumWays(vector<int>& nums, int target) {int sum 0;for(int i 0; i < nums.size(); i){sum nums[i];}if(abs(target) > sum)…...

vue3中自定义校验函数密码不生效问题
vue3中自定义校验函数密码不生效问题 由于在自定义的校验规则中只校验了有数据的情况,以至于在没输入时,校验不生效 (1)用户不输入校验不生效 const validateSurePassword (rule, value, callback) > {if (value ! ) {if (…...

RabbitMQ(死信队列)
一、本文抒写背景 前面我也在延迟队列篇章提到过死信队列,也提到过一些应用场景! 今天呢,这篇文章,主要就是实战一个业务场景的小Demo流程,哈哈,那就是延迟关闭订单。 二、开始啦!letgo! 首…...

HTTP代理的优点和局限性
在这个信息爆炸的时代,网络已成为我们获取知识、交流思想、开展商务的重要平台。但随之而来的隐私泄露、网络安全威胁、以及无处不在的网络监控,却让我们的每一次在线活动都充满了风险。 在这样的背景下,HTTP代理技术应运而生,它不…...

大厂面试真题-如果通过JVM自带的工具排查和解决线上CPU100%的问题
通过JVM自带的工具去定位和解决线上CPU 100%的问题,可以遵循以下步骤: 一、使用top和jps定位Java进程 使用top命令: 在Linux服务器上执行top命令,查看所有进程的CPU使用情况。找到CPU使用率最高的进程,并记录其PID&a…...

kubernetes中微服务部署
微服务 问:用控制器来完成集群的工作负载,那么应用如何暴漏出去? 答:需要通过微服务暴漏出去后才能被访问 Service 是一组提供相同服务的Pod对外开放的接口借助Service,应用可以实现服务发现和负载均衡Service 默认只…...

基于 Java 的天气预报系统设计与实现
随着互联网的飞速发展,天气预报系统变得越来越重要。它可以帮助用户了解未来几天的天气情况,便于出行、活动安排。本文将介绍如何使用 Java 构建一个简单的天气预报系统,涉及系统架构设计、核心功能开发以及完整的代码实现。 1. 系统架构设计…...

思迅商云8前台打开提示上传日志信息失败
请按照以下步骤核实处理: 1、重启sql服务后测试。 2、请先备份前台安装目录,之后删除安装目录下的log文件和localdate下的log文件,之后重新打开软件,若依旧不行则说明前台文件有损坏,需要重新安装客户端,…...

webstorm的缩进设置(过度缩进解释)
在编写前端代码时 缩进规范一般被认为是2个空格 而非默认的4个空格 当我们通过webstorm去编写前端代码时 我们可以通过setting->Code Style->html/css/js指定的界面中去设置tab/indent/continuation indent 具体的话 我们将html/css/js操作界面中的tab/indent设置为2个空…...

与ZoomEye功能类似的搜索引擎还有哪些?(渗透课作业)
与ZoomEye功能类似的搜索引擎有: Shodan:被誉为“物联网的搜索引擎”,专注于扫描和索引连接到互联网的各种设备,如智能家居设备、工业控制系统、摄像头、数据库等。它提供全球互联网设备的可视化视图,帮助用户了解网络…...

Java 计数排序
计数排序(Counting Sort)是一种非比较型排序算法,适用于一定范围内的整数排序。它的基本思想是通过计数输入元素中每个值出现的次数,然后计算每个值的起始位置,最终将元素放到正确的位置上。计数排序的时间复杂度为 O(…...

error: RPC failed; curl 16 Error in the HTTP2 framing layer
yschai@LAPTOP-F2L146JK:~$ git clone https://github.com/Chyusen/yschai.git Cloning into ‘yschai’… error: RPC failed; curl 16 Error in the HTTP2 framing layer fatal: expected flush after ref listing 使用Ubuntu在git clone github上的项目的时候,遇到以上报错…...

Python脚本分类和代码举例
Python是一种强大且灵活的编程语言,被广泛应用于数据分析、Web开发、自动化、人工智能等领域。在不同的应用场景下,Python脚本可以被分类为多种类型。本文将深入分析Python脚本的分类,同时提供相关代码示例,帮助读者理解和应用这些…...

【Redis十二】Redis的典型应用(缓存和分布式锁)
目录 Redis作为缓存 1.什么是缓存? 2.缓存的更新策略 3.缓存预热,缓存穿透,缓存雪崩和缓存击穿 Redis作为分布式锁 1.什么是分布式锁? 2.分布式锁的实现过程 Redis是目前后端开发中非常热门的组件之一,本篇文章…...

C++入门基础知识107—【关于C++continue 语句】
成长路上不孤单😊😊😊😊😊😊 【14后😊///C爱好者😊///持续分享所学😊///如有需要欢迎收藏转发///😊】 今日分享关于C continue 语句的相关内容!…...