经验分享:用一张表解决并发冲突!数据库事务锁的核心实现逻辑
背景
对于一些内部使用的管理系统来说,可能没有引入Redis,又想基于现有的基础设施处理并发问题,而数据库是每个应用都避不开的基础设施之一,因此分享个我曾经维护过的一个系统中,使用数据库表来实现事务锁的方式。
之前在文章Java业务功能并发问题处理中实现了使用
MySQL行锁、Redis分布式锁来处理业务并发问题,这次来填坑了,如果想了解其他并发问题处理方式和区别,可以看看文章Java业务功能并发问题处理哈。
业务流程说明

方案分析
适用场景
- 应用服务有多个实例,但是数据库是单实例;
- 没有用上
Redis的应用服务,想通过现有的基础设施解决并发数据问题
待改进措施
- 设置超时机制:当出现锁无法及时释放时需要手动删除表数据,可以设置逻辑删除字段或者定时器删除过期数据;
- 重试获取锁机制:设置一定的循环次数,当获取不到锁时休眠200毫秒再次获取,直到循环次数用尽后再返回失败;
- 锁重入支持:通过增加
加锁次数字段让当同一个线程可以重复获取锁
程序实现过程
框架及工具说明
- 技术框架:
SpringBoot、MyBatis、Maven - 数据库:
MySQL - 测试工具:
Apifox - 表设计及代码说明:
- 唯一索引:需要有一个用于判断唯一的字段,在数据库表中通过指定唯一索引来实现;
- 加锁的线程号:避免A线程加的锁,被B线程删除;
- 锁的可见性要单独事务:添加事务锁的逻辑应在我们执行业务逻辑的事务之前,且不能跟业务逻辑的事务在一块,否则在事务提交前其他线程根本看不到这个锁,也就达不到我们锁的目的了;
- 为了我们的锁更方便使用,也可以将加锁逻辑抽到注解中实现,注解的实现流程:
- 在pom文件中引入
spring-boot-starter-aop - 编写自定义注解
ConcurrencyLock - 实现切面类(
Aspect)逻辑
- 在pom文件中引入
代码展示
为了能让大家更关注加解锁逻辑,本文只保留主要代码,参考链接处会放置
码云(gitee)的源码地址(或者点击此处跳转);
另外,本文就不展示注解方式的使用了,以免占用篇幅。
代码结构图

实体类
/*** 并发锁实体类*/
public class ConcurrencyLockBean {/*** 数据库主键*/private Long id;/*** 操作节点*/private String businessNode;/*** 订单唯一编号*/private String businessUniqueNo;/*** 线程ID*/private Long threadId;/*** 创建日期*/private Date creationDate;
}/*** 订单实体类*/
@Setter
@Getter
@ToString
public class OrderInfoBean {/*** 自增长主键*/private int id;/*** 订单号*/private String orderNo;/*** 物料数量*/private Integer itemQty;
}
ConcurrencyLockServiceImpl.java
@Slf4j
@Service
public class ConcurrencyLockServiceImpl implements ConcurrencyLockService {ConcurrencyLockMapper mapper;/*** service类注入*/@AutowiredConcurrencyLockServiceImpl(ConcurrencyLockMapper mapper) {this.mapper = mapper;}@Overridepublic Boolean tryLock(String businessNode, String businessUniqueNo) {long threadId = Thread.currentThread().getId();ConcurrencyLockBean concurrencyLock = mapper.selectConcurrencyLock(businessNode, businessUniqueNo);if (concurrencyLock != null) {log.info("{}数据正在操作中,请稍后", threadId);return false;}ConcurrencyLockBean lock = new ConcurrencyLockBean();lock.setBusinessNode(businessNode);lock.setBusinessUniqueNo(businessUniqueNo);lock.setThreadId(threadId);try {int insertCount = mapper.insertConcurrencyLock(lock);if (insertCount == 0) {log.info("{}获取锁失败,请稍后重试", threadId);return false;}} catch (Exception e) {log.info("{}获取锁异常,请稍后重试", threadId);return false;}log.info("{}完成锁表插入", threadId);return true;}@Overridepublic void unLock(String businessNode, String businessUniqueNo) {ConcurrencyLockBean lock = new ConcurrencyLockBean();long threadId = Thread.currentThread().getId();lock.setThreadId(threadId);lock.setBusinessNode(businessNode);lock.setBusinessUniqueNo(businessUniqueNo);mapper.deleteConcurrencyLock(lock);log.info("{}执行解锁完毕", threadId);}
}
ConcurrencyLockMapper.java
import org.apache.ibatis.annotations.Param;public interface ConcurrencyLockMapper {/*** 根据业务节点和唯一业务号查询锁*/ConcurrencyLockBean selectConcurrencyLock(@Param("businessNode") String businessNode, @Param("businessUniqueNo") String businessUniqueNo);/*** 插入锁*/int insertConcurrencyLock(ConcurrencyLockBean lock);/*** 删除锁*/int deleteConcurrencyLock(ConcurrencyLockBean lock);
}
ConcurrencyLockMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.runningrookie.mapper.ConcurrencyLockMapper"><select id="selectConcurrencyLock" resultType="com.runningrookie.domain.ConcurrencyLockBean">SELECTTHREAD_ID,BUSINESS_NODE,BUSINESS_UNIQUE_NO,CREATION_DATEFROM concurrency_lockWHERE BUSINESS_UNIQUE_NO = #{businessUniqueNo}AND BUSINESS_NODE = #{businessNode}</select><insert id="insertConcurrencyLock" useGeneratedKeys="true" keyProperty="id">INSERT INTO concurrency_lock (THREAD_ID,BUSINESS_NODE,BUSINESS_UNIQUE_NO,CREATION_DATE)VALUES(#{threadId}, #{businessNode}, #{businessUniqueNo}, NOW());</insert><delete id="deleteConcurrencyLock">DELETE FROM concurrency_lockWHERE THREAD_ID = #{threadId}and BUSINESS_NODE = #{businessNode}and BUSINESS_UNIQUE_NO = #{businessUniqueNo}</delete>
</mapper>
ConcurrencyLock.java注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConcurrencyLock {String businessNode();String businessUniqueNoKey();
}
ConcurrencyLockAspect.java注解类
@Aspect
@Component
@Slf4j
public class ConcurrencyLockAspect {ConcurrencyLockService concurrencyLockService;@AutowiredConcurrencyLockAspect(ConcurrencyLockService concurrencyLockService) {this.concurrencyLockService = concurrencyLockService;}// 环绕切面@Around("@annotation(concurrencyLock)")public Object around(ProceedingJoinPoint joinPoint, ConcurrencyLock concurrencyLock) throws Throwable {long threadId = Thread.currentThread().getId();Object[] args = joinPoint.getArgs();if (args.length == 0) {return joinPoint.proceed();}// 通过反射获取值String invokeMethodName = "get" + concurrencyLock.businessUniqueNoKey().substring(0, 1).toUpperCase() + concurrencyLock.businessUniqueNoKey().substring(1);// 获取Order类的Class对象Class<?> clazz = args[0].getClass();// 获取getOrderNo方法的Method对象Method method = clazz.getMethod(invokeMethodName);// 调用getOrderNo方法并获取返回值String businessUniqueNo = method.invoke(args[0]).toString();Boolean isSuccessLock = concurrencyLockService.tryLock(concurrencyLock.businessNode(), businessUniqueNo);if (!isSuccessLock) {log.info("{}加锁失败,请稍后重试", threadId);// 生成与切点方法相同的返回对象return AjaxResult.error("加锁失败,请稍后重试");}try {log.info("{}开始执行业务逻辑", threadId);joinPoint.proceed();} finally {concurrencyLockService.unLock(concurrencyLock.businessNode(), businessUniqueNo);}return joinPoint.proceed();}
}
OrderInfoController.java
@RestController
@RequestMapping("/orderInfo")
public class OrderInfoController {OrderInfoService orderInfoService;@Autowiredprivate OrderInfoController(OrderInfoService orderInfoService) {this.orderInfoService = orderInfoService;}@PostMappingpublic AjaxResult saveOrderInfo(@RequestBody OrderInfoBean bean) {return orderInfoService.saveOrderInfo(bean);}
}
OrderServiceImpl.java
/*** 订单逻辑代码*/
@Slf4j
@Service
public class OrderInfoServiceImpl implements OrderInfoService {ConcurrencyLockService concurrencyLockService;/*** service类注入*/@AutowiredOrderInfoServiceImpl(ConcurrencyLockService concurrencyLockService) {this.concurrencyLockService = concurrencyLockService;}@Overridepublic AjaxResult saveOrderInfo(OrderInfoBean bean) {long threadId = Thread.currentThread().getId();final String businessNode = "插入";Boolean isSuccessLock = concurrencyLockService.tryLock(businessNode, bean.getOrderNo());if (!isSuccessLock) {return AjaxResult.error("加锁失败,请稍后重试");}try {log.info("{}开始执行业务逻辑", threadId);// TODO:模拟业务逻辑耗时Thread.sleep(1500);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {concurrencyLockService.unLock(businessNode, bean.getOrderNo());}return AjaxResult.success();}@Override@ConcurrencyLock(businessNode = "插入", businessUniqueNoKey = "orderNo")@Transactionalpublic AjaxResult saveOrderInfoByAnnotation(OrderInfoBean bean) {// TODO:模拟业务逻辑耗时Thread.sleep(1500);return AjaxResult.success();}
}
pom.xml相关依赖
在dependencies中添加下列依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.4</version>
</dependency>
<!-- Mysql驱动包 -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId>
</dependency>
事务处理表的表结构
CREATE TABLE `concurrency_lock` (`ID` int NOT NULL AUTO_INCREMENT COMMENT '主键',`THREAD_ID` int DEFAULT NULL COMMENT '线程号',`BUSINESS_NODE` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '操作节点',`BUSINESS_UNIQUE_NO` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '单据号',`CREATION_DATE` datetime DEFAULT NULL COMMENT '创建时间',PRIMARY KEY (`ID`),UNIQUE KEY `uni_business_no` (`BUSINESS_UNIQUE_NO`,`BUSINESS_NODE`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
测试输出结果
使用Apifox并发发送5次请求,可以看到实际成功获取到锁并执行的只有一个线程
17:08:00.449 [http-nio-8080-exec-1] c.r.service.impl.ConcurrencyLockServiceImpl - 40完成锁表插入
17:08:00.462 [http-nio-8080-exec-1] c.runningrookie.service.impl.OrderInfoServiceImpl - 40开始执行业务逻辑
17:08:00.573 [http-nio-8080-exec-5] c.r.service.impl.ConcurrencyLockServiceImpl - 44获取锁异常,请稍后重试
17:08:00.573 [http-nio-8080-exec-4] c.r.service.impl.ConcurrencyLockServiceImpl - 43获取锁异常,请稍后重试
17:08:00.573 [http-nio-8080-exec-3] c.r.service.impl.ConcurrencyLockServiceImpl - 42获取锁异常,请稍后重试
17:08:00.573 [http-nio-8080-exec-2] c.r.service.impl.ConcurrencyLockServiceImpl - 41获取锁异常,请稍后重试
17:08:00.574 [http-nio-8080-exec-1] c.r.service.impl.ConcurrencyLockServiceImpl - 40执行解锁完毕
参考链接
gitee代码仓库地址:数据库并发锁
相关文章:
经验分享:用一张表解决并发冲突!数据库事务锁的核心实现逻辑
背景 对于一些内部使用的管理系统来说,可能没有引入Redis,又想基于现有的基础设施处理并发问题,而数据库是每个应用都避不开的基础设施之一,因此分享个我曾经维护过的一个系统中,使用数据库表来实现事务锁的方式。 之…...
嵌入式学习前要了解的基础知识
一、电压和电流 在嵌入式开发中,电压和电流是两个基本的电气概念,对于理解和设计电子电路至关重要。它们直接影响到嵌入式系统的性能、功耗、可靠性和安全性。 电压(Voltage) 电压是电场力推动电荷移动的能力,通常以…...
RTC、直播、点播技术对比|腾讯云/即构/声网如何选型 — 2025 版
前言 作为一个有多年实战经验的开发者,在音视频技术领域我深刻体会到 RTC(实时通信)、直播和点播三者的不同。虽然它们的核心都涉及音视频内容的传输,但在实际应用中,它们的技术实现、使用场景以及所面临的挑战各不相…...
《白帽子讲 Web 安全》之文件操作安全
目录 引言 (一)文件上传与下载漏洞概述 1.文件上传的常见安全隐患 1.1前端校验的脆弱性与服务端脚本执行危机在文件上传流程中,部分开发者可能会在前端使用 JavaScript 代码对文件后缀名进行简单校验,试图以此阻止非法文件上传…...
yolov8训练模型、测试视频
yolov8先训练生成best.pt文件,用这个生成的模型进行视频的测试 因为本来用的代码生成的测试视频打不开,格式应该是损坏了,或者部分帧没有正常保存吧。 修改了一下代码,现状可以正常打开生成的视频了。 1、训练代码train.py im…...
03.网络编程套接字(二)
文章目录 简单的TCP网络程序 服务端创建套接字 服务端绑定 服务端监听 服务端获取连接 服务端处理请求 客户端创建套接字 客户端发起请求 服务器测试 单执行流服务器的弊端 多进程版的TCP网络程序 线程池版的TCP网络程序 简单的TCP网络程序 服务端创建套接字 我…...
一周学会Flask3 Python Web开发-Flask3之表单处理WTForms安装与定义WTForms表单类
锋哥原创的Flask3 Python Web开发 Flask3视频教程: 2025版 Flask3 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili 我们平时开发项目,都会用到表单,编写表单,提交表单,验证表单,如果…...
Git基本命令索引
GIT基本命令索引 创建代码库修改和提交代码日志管理远程操作操作分支 创建代码库 操作指令初始化仓库git init克隆远程仓库git clone 修改和提交代码 操作指令查看文件状态git status文件暂存git add文件比较git diff文件提交git commit回滚版本git reset重命名或者移动工作…...
【论文阅读笔记】SL-YOLO(2025/1/13) | 小目标检测 | HEPAN、C2fDCB轻量化模块
目录 摘要 1 引言 2 相关工作 3 方法 3.1 为小目标检测增加一个头 3.2 优化网络结构 3.3 改进轻量化模块 3.3.1 C2fDCB 3.3.2 SCDown 4 实验 4.1 数据集 4.2 实验环境 4.3 与其他模型的比较 4.4 消融研究 ▲不同网络结构的分析 ▲不同模块的分析 ▲不同降采样…...
MySQL SQL 优化专题
MySQL SQL 优化专题 1. 插入数据优化 -- 普通插入(不推荐) INSERT INTO tb_user VALUES(1,tom); INSERT INTO tb_user VALUES(2,cat); INSERT INTO tb_user VALUES(3,jerry);-- 优化方案1:批量插入(推荐,不建议超过1…...
Mac上安装Pycharm
说明:仅供参考,是自己的安装流程,以免以后自己想不起来来看看的笔记 官网地址:https://www.jetbrains.com/pycharm/ 1、点击Download,跳转到下一个页面 2、MAC,选择Mac OS,在Pycharm Professio…...
flask框架基础入门学习教程
文章目录 前言1. 环境搭建1.1Python安装1.2选择Python开发环境1.3 创建虚拟环境(可选但推荐)1.4 安装 Flask 2. 第一个 Flask 应用3. 路由和视图函数3.1 基本路由3.2 动态路由3.3 路由参数类型 4. 请求和响应4.1 获取请求数据4.2 响应对象 5. 模板渲染6.…...
Qt显示一个hello world
一、显示思路 思路一:通过图形化方式,界面上创建出一个控件显示。 思路二:通过编写C代码在界面上创建控件显示。 二、思路一实现 点开 Froms 的 widget.ui,拖拽 label 控件,显示 hello world 即可。 qmake 基于 .…...
MySQL快速搭建主从复制
一、基于位点的主从复制部署流程 确定主库Binlog是否开启修改主从server_id主库导出数据从库导入数据确定主库备份时的位点在从库配置主库信息查看复制状态并测试数据是否同步 二、准备阶段(主库和从库配置都需要修改) 1、确定主库Binlog是否开启 2、修改主从se…...
力扣-动态规划-674 最长连续递增序列
思路 dp数组定义:以i为结尾的最长连续递增序列递推公式: if(nums[i-1] < nums[i]) dp[i] dp[i-1] 1; dp数组初始化:都为1遍历顺序:顺序时间复杂度: 代码 class Solution { public:int findLengthOfLCIS(v…...
在笔记本电脑上用DeepSeek搭建个人知识库
最近DeepSeek爆火,试用DeepSeek的企业和个人越来越多。最常见的应用场景就是知识库和知识问答。所以本人也试用了一下,在笔记本电脑上部署DeepSeek并使用开源工具搭建一套知识库,实现完全在本地环境下使用本地文档搭建个人知识库。操作过程共…...
leetcode 94. 二叉树的中序遍历
题目如下 做了那么多道难题,给自己放放松。通过代码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int …...
YOLOv12:目标检测新时代的破局者
目录 一、YOLOv12 横空出世二、YOLOv12 的性能飞跃2.1 多规模优势2.2 对比超越 三、技术创新与原理剖析3.1 区域注意力模块(Area Attention,A2)3.2 残差高效层聚合网络(R-ELAN)3.3 架构优化细节 四、实验验证与结果分析…...
基于OFDR的层压陆相页岩油储层中非对称裂缝群传播的分布式光纤监测
关键词:OFDR、分布式光纤传感、裂缝传播 一. 概述 四川盆地凉高山组优质页岩油储层存在复杂的垂直重叠岩性,大陆页岩油储层存在发育层理,薄层和天然裂缝,对水平井多级压裂技术的裂缝网络形态控制和监测构成挑战。本研究提出了一…...
Linux虚拟机网络配置-桥接网络配置
简介 本文档旨在指导用户如何在虚拟环境中配置Linux系统的桥接网络,以实现虚拟机与物理主机以及外部网络的直接通信。桥接网络允许虚拟机如同一台独立的物理机一样直接连接到物理网络,从而可以被分配一个独立的IP地址,并能够与网络中的其他设…...
脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)
一、数据处理与分析实战 (一)实时滤波与参数调整 基础滤波操作 60Hz 工频滤波:勾选界面右侧 “60Hz” 复选框,可有效抑制电网干扰(适用于北美地区,欧洲用户可调整为 50Hz)。 平滑处理&…...
基于Flask实现的医疗保险欺诈识别监测模型
基于Flask实现的医疗保险欺诈识别监测模型 项目截图 项目简介 社会医疗保险是国家通过立法形式强制实施,由雇主和个人按一定比例缴纳保险费,建立社会医疗保险基金,支付雇员医疗费用的一种医疗保险制度, 它是促进社会文明和进步的…...
关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案
问题描述:iview使用table 中type: "index",分页之后 ,索引还是从1开始,试过绑定后台返回数据的id, 这种方法可行,就是后台返回数据的每个页面id都不完全是按照从1开始的升序,因此百度了下,找到了…...
系统设计 --- MongoDB亿级数据查询优化策略
系统设计 --- MongoDB亿级数据查询分表策略 背景Solution --- 分表 背景 使用audit log实现Audi Trail功能 Audit Trail范围: 六个月数据量: 每秒5-7条audi log,共计7千万 – 1亿条数据需要实现全文检索按照时间倒序因为license问题,不能使用ELK只能使用…...
最新SpringBoot+SpringCloud+Nacos微服务框架分享
文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...
五年级数学知识边界总结思考-下册
目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解:由来、作用与意义**一、知识点核心内容****二、知识点的由来:从生活实践到数学抽象****三、知识的作用:解决实际问题的工具****四、学习的意义:培养核心素养…...
C# 类和继承(抽象类)
抽象类 抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。 不能创建抽象类的实例。抽象类使用abstract修饰符声明。 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带 实现的成员的任意组合。抽象类自己可以派生自另一个抽象类。例…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
图表类系列各种样式PPT模版分享
图标图表系列PPT模版,柱状图PPT模版,线状图PPT模版,折线图PPT模版,饼状图PPT模版,雷达图PPT模版,树状图PPT模版 图表类系列各种样式PPT模版分享:图表系列PPT模板https://pan.quark.cn/s/20d40aa…...
均衡后的SNRSINR
本文主要摘自参考文献中的前两篇,相关文献中经常会出现MIMO检测后的SINR不过一直没有找到相关数学推到过程,其中文献[1]中给出了相关原理在此仅做记录。 1. 系统模型 复信道模型 n t n_t nt 根发送天线, n r n_r nr 根接收天线的 MIMO 系…...
