经验分享:用一张表解决并发冲突!数据库事务锁的核心实现逻辑

news2025/3/6 20:15:49

背景

对于一些内部使用的管理系统来说,可能没有引入Redis,又想基于现有的基础设施处理并发问题,而数据库是每个应用都避不开的基础设施之一,因此分享个我曾经维护过的一个系统中,使用数据库表来实现事务锁的方式。

之前在文章Java业务功能并发问题处理中实现了使用MySQL行锁、Redis分布式锁来处理业务并发问题,这次来填坑了,如果想了解其他并发问题处理方式和区别,可以看看文章Java业务功能并发问题处理哈。

业务流程说明

业务加锁流程图

方案分析

适用场景

  1. 应用服务有多个实例,但是数据库是单实例;
  2. 没有用上Redis的应用服务,想通过现有的基础设施解决并发数据问题

待改进措施

  1. 设置超时机制:当出现锁无法及时释放时需要手动删除表数据,可以设置逻辑删除字段或者定时器删除过期数据;
  2. 重试获取锁机制:设置一定的循环次数,当获取不到锁时休眠200毫秒再次获取,直到循环次数用尽后再返回失败;
  3. 锁重入支持:通过增加加锁次数字段让当同一个线程可以重复获取锁

程序实现过程

框架及工具说明

  • 技术框架:SpringBootMyBatisMaven
  • 数据库:MySQL
  • 测试工具:Apifox
  • 表设计及代码说明:
    1. 唯一索引:需要有一个用于判断唯一的字段,在数据库表中通过指定唯一索引来实现;
    2. 加锁的线程号:避免A线程加的锁,被B线程删除;
    3. 锁的可见性要单独事务:添加事务锁的逻辑应在我们执行业务逻辑的事务之前,且不能跟业务逻辑的事务在一块,否则在事务提交前其他线程根本看不到这个锁,也就达不到我们锁的目的了;
    4. 为了我们的锁更方便使用,也可以将加锁逻辑抽到注解中实现,注解的实现流程:
      • 在pom文件中引入spring-boot-starter-aop
      • 编写自定义注解ConcurrencyLock
      • 实现切面类(Aspect)逻辑

代码展示

为了能让大家更关注加解锁逻辑,本文只保留主要代码,参考链接处会放置码云(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类注入
     */
    @Autowired
    ConcurrencyLockServiceImpl(ConcurrencyLockMapper mapper) {
        this.mapper = mapper;
    }

    @Override
    public 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;
    }

    @Override
    public 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">
        SELECT
            THREAD_ID,
            BUSINESS_NODE,
            BUSINESS_UNIQUE_NO,
            CREATION_DATE
        FROM concurrency_lock
        WHERE 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_lock
        WHERE 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;
    @Autowired
    ConcurrencyLockAspect(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;
    @Autowired
    private OrderInfoController(OrderInfoService orderInfoService) {
        this.orderInfoService = orderInfoService;
    }
    @PostMapping
    public AjaxResult saveOrderInfo(@RequestBody OrderInfoBean bean) {
        return orderInfoService.saveOrderInfo(bean);
    }
}

OrderServiceImpl.java

/**
 * 订单逻辑代码
 */
@Slf4j
@Service
public class OrderInfoServiceImpl implements OrderInfoService {
    ConcurrencyLockService concurrencyLockService;
    /**
     * service类注入
     */
    @Autowired
    OrderInfoServiceImpl(ConcurrencyLockService concurrencyLockService) {
        this.concurrencyLockService = concurrencyLockService;
    }
    @Override
    public 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")
    @Transactional
    public 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代码仓库地址:数据库并发锁

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2310113.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++-第二十章:智能指针

目录 第一节&#xff1a;std::auto_ptr 第二节&#xff1a;std::unique_ptr 第三节&#xff1a;std::shared_ptr 第四节&#xff1a;std::shared_ptr的缺陷 4-1.循环引用 4-2.删除器 下期预告&#xff1a; 智能指针的作用是防止指针出作用域时忘记释放内存而造成内存泄漏&…

chrome Vue.js devtools 提示不支持该扩展组件,移除

可能是版本不兼容&#xff0c;可以重新安装&#xff0c;推荐网址极简插件官网_Chrome插件下载_Chrome浏览器应用商店 直接搜索vue&#xff0c;下载旧版&#xff0c;vue2、vue3都支持&#xff0c;上面那个最新版本试了下&#xff0c;vue2的肯定是不能用

C# 中的Action和Func是什么?Unity 中的UnityAction是什么? 他们有什么区别?

所属范围&#xff1a;Action 和 Func 是 C# 语言标准库中的委托类型&#xff0c;可在任何 C# 项目里使用&#xff1b;UnityAction 是 Unity 引擎专门定义的委托类型&#xff0c;只能在 Unity 项目中使用。 返回值&#xff1a;Action 和 UnityAction 封装的方法没有返回值&…

Versal - XRT(CPP) 2024.1

目录 1.简介 2. XRT 2.1 XRT vs OpenCL 2.2 Takeways 2.3 XRT C APIs 2.4 Device and XCLBIN 2.5 Buffers 2.5.1 Buffer 创建 2.5.1.1 普通 Buffer 2.5.1.2 特殊 Buffer 2.5.1.3 用户指针 Buffer 2.5.2 Data Transfer 2.5.2.1 read/write API 2.5.2.2 map API 2…

windows下安装Open Web UI

windows下安装openwebui有三种方式,docker,pythonnode.js,整合包. 这里我选择的是第二种,非docker. 非Docker方式安装 1. 安装Python&#xff1a; 下载并安装Python 3.11&#xff0c;建议安装路径中不要包含中文字符&#xff0c;并勾选“Add python 3.11 to Path”选项。 安…

【自用】NLP算法面经(4)

一、deepseek 1、MLA &#xff08;1&#xff09;LLM推理过程 prefill阶段&#xff1a;模型对全部的prompt tokens一次性并行计算&#xff0c;最终生成第一个输出token。decode阶段&#xff1a;每次生成一个token&#xff0c;直到生成EOS&#xff08;end-of-sequence&#xf…

LeetCode热题100JS(20/100)第四天|​41. 缺失的第一个正数​|​73. 矩阵置零​|​54. 螺旋矩阵​|​48. 旋转图像​

41. 缺失的第一个正数 题目链接&#xff1a;41. 缺失的第一个正数 难度&#xff1a;困难 刷题状态&#xff1a;1刷 新知识&#xff1a; 解题过程 思考 示例 1&#xff1a; 输入&#xff1a;nums [1,2,0] 输出&#xff1a;3 解释&#xff1a;范围 [1,2] 中的数字都在数组中…

【银河麒麟高级服务器操作系统实际案例分享】数据库资源重启现象分析及处理全过程

更多银河麒麟操作系统产品及技术讨论&#xff0c;欢迎加入银河麒麟操作系统官方论坛 https://forum.kylinos.cn 了解更多银河麒麟操作系统全新产品&#xff0c;请点击访问 麒麟软件产品专区&#xff1a;https://product.kylinos.cn 开发者专区&#xff1a;https://developer…

开源架构与人工智能的融合:开启技术新纪元

最近五篇文章推荐&#xff1a; 开源架构的自动化测试策略优化版&#xff08;New&#xff09; 开源架构的容器化部署优化版&#xff08;New&#xff09; 开源架构的微服务架构实践优化版&#xff08;New&#xff09; 开源架构中的数据库选择优化版&#xff08;New&#xff09; 开…

【弹性计算】弹性裸金属服务器和神龙虚拟化(二):适用场景

《弹性裸金属服务器》系列&#xff0c;共包含以下文章&#xff1a; 弹性裸金属服务器和神龙虚拟化&#xff08;一&#xff09;&#xff1a;功能特点弹性裸金属服务器和神龙虚拟化&#xff08;二&#xff09;&#xff1a;适用场景弹性裸金属服务器和神龙虚拟化&#xff08;三&a…

计算机毕业设计SpringBoot+Vue.js保险合同管理系统(源码+文档+PPT+讲解)

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

58、深度学习-自学之路-自己搭建深度学习框架-19、RNN神经网络梯度消失和爆炸的原因(从公式推导方向来说明),通过RNN的前向传播和反向传播公式来理解。

一、RNN神经网络的前向传播图如下&#xff1a; 时间步 t1: x₁ → (W_x) → [RNN Cell] → h₁ → (W_y) → y₁ ↑ (W_h) h₀ (初始隐藏状态) 时间步 t2: x₂ → (W_x) → [RNN Cell] → h₂ → (W_y) → y₂ ↑ (W_h) h₁ 时间…

bge-large-zh-v1.5 与Pro/BAAI/bge-m3 区别

ge-large-zh-v1.5 和 Pro/BAAI/bge-m3 是两种不同的模型&#xff0c;主要区别在于架构、性能和应用场景。以下是它们的对比&#xff1a; 1. 模型架构 bge-large-zh-v1.5&#xff1a; 基于Transformer架构&#xff0c;专注于中文文本的嵌入表示。 参数量较大&#xff0c;适合处…

lamp平台介绍

一、lamp介绍 网站&#xff1a; 静态 动态 php语言 .php 作用&#xff1a;运行php语言编写动态网站应用 lamp Linux Apache MySQL PHP PHP是作为httpd的一个功能模块存在的 二、部署lamp平台 1、测试httpd是否可正常返回PHP的响应 2、测试PHP代码是否可正常连接数据…

Windows10系统构建本地安全私有化的个人知识库——采用DeepSeek+RAGFlow

一、为什么要构建本地私有化个人知识库 1.1、自身需求 1、需要相关隐私资料内容的安全保护可控; 2、需要根据自身的隐私资料内容构建出个性化的知识库; 一些常见的业务场景如:①希望我们的智能助手可以根据公司的管理制度回答问题,让员工可以随时了解公司相关制度内容信息;…

Linux: Centos7 Cannot find a valid baseurl for repo: base/7/x86_64 解决方案

Linux: Centos7 Cannot find a valid baseurl for repo: base/7/x86_64 解决方案 问题背景&#xff1a; 执行yum update出现如下报错 排查虚拟机是否联网&#xff1a; ping -c 4 www.baidu.com 可以看到网络链接没有问题 解决方案&#xff1a; 原因是国外的镜像源有问题&am…

Java 大视界 -- Java 大数据在智能金融反欺诈中的技术实现与案例分析(114)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

每日OJ_牛客_游游的字母串_枚举_C++_Java

目录 牛客_游游的字母串_枚举 题目解析 C代码 Java代码 牛客_游游的字母串_枚举 游游的字母串 描述&#xff1a; 对于一个小写字母而言&#xff0c;游游可以通过一次操作把这个字母变成相邻的字母。a和b相邻&#xff0c;b和c相邻&#xff0c;以此类推。特殊的&#xff0…

示例:在WPF中如何使用Segoe MDL2 Assets图标和使用该图标的好处

一、目的&#xff1a;分享在WPF中如何使用Segoe MDL2 Assets图标和使用该图标的好处 在WPF中使用Segoe MDL2 Assets字体&#xff0c;可以通过设置控件的FontFamily属性来实现。Segoe MDL2 Assets是一个包含许多图标的字体&#xff0c;通常用于Windows应用程序的图标显示。 二、…

h5 IOS端渐变的兼容问题 渐变实现弧形效果

IOS端使用渐变的时候有兼容问题 以下是问题效果&#xff0c;图中黑色部分期望的效果应该是白色的。但是ios端是下面的样子…… 安卓pc 支持&#xff1a; background-image: radial-gradient(circle 40rpx at 100% 0, #f3630c 40rpx, rgb(255, 255, 255) 50%);安卓pc ios支持…