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

背景

对于一些内部使用的管理系统来说,可能没有引入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代码仓库地址:数据库并发锁

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【 实战案例篇三】【某金融信息系统项目管理案例分析】

大家好,今天咱们来聊聊金融行业的信息系统项目管理。这个话题听起来可能有点专业,但别担心,我会尽量用大白话给大家讲清楚。金融行业的信息系统项目管理,说白了就是如何高效地管理那些复杂的IT项目,确保它们按时、按预算、按质量完成。咱们今天不仅会聊到一些理论,还会通…

爬虫系列之发送请求与响应《一》

一、请求组成 1.1 请求方式&#xff1a;GET和POST请求 GET:从服务器获取&#xff0c;请求参数直接附在URL之后&#xff0c;便于查看和分享&#xff0c;常用于获取数据和查询操作 POST&#xff1a;用于向服务器提交数据&#xff0c;其参数不会显示在URL中&#xff0c;而是包含在…

最新最详细的配置Node.js环境教程

配置Node.js环境 一、前言 &#xff08;一&#xff09;为什么要配置Node.js&#xff1f;&#xff08;二&#xff09;NPM生态是什么&#xff08;三&#xff09;Node和NPM的区别 二、如何配置Node.js环境 第一步、安装环境第二步、安装步骤第三步、验证安装第四步、修改全局模块…

题解 | 牛客周赛83 Java ABCDEF

目录 题目地址 做题情况 A 题 B 题 C 题 D 题 E 题 F 题 牛客竞赛主页 题目地址 牛客竞赛_ACM/NOI/CSP/CCPC/ICPC算法编程高难度练习赛_牛客竞赛OJ 做题情况 A 题 输出两个不是同一方位的字符中的任意一个就行 import java.io.*; import java.math.*; import java…

netty如何处理粘包半包

文章目录 NIO中存在问题粘包半包滑动窗口MSS 限制Nagle 算法 解决方案 NIO中存在问题 粘包 现象&#xff0c;发送 abc def&#xff0c;接收 abcdef原因 应用层&#xff1a;接收方 ByteBuf 设置太大&#xff08;Netty 默认 1024&#xff09;滑动窗口&#xff1a;假设发送方 25…

【Linux】I/O操作

目录 1. 整体学习思维导图 2. 理解文件 2.1 文件是什么&#xff1f; 2.2 回顾C语言库函数的文件操作 2.3 stdin/stdout/stderr 2.4 系统的文件I/O操作 2.4.1 了解位图标记位方法(宏) 2.4.2 认识系统I/O常用调用接口 2.5 对比C文件操作函数和系统调用函数 2.5.1 fd是什么…

ISP CIE-XYZ色彩空间

1. 颜色匹配实验 1931年&#xff0c;CIE综合了前人实验数据&#xff0c;统一采用700nm&#xff08;红&#xff09;、546.1nm&#xff08;绿&#xff09;、435.8nm&#xff08;蓝&#xff09;​作为标准三原色波长&#xff0c;绘制了色彩匹配函数&#xff0c;如下图。选定这些波…

5G学习笔记之BWP

我们只会经历一种人生&#xff0c;我们选择的人生。 参考&#xff1a;《5G NR标准》、《5G无线系统指南:如微见著&#xff0c;赋能数字化时代》 目录 1. 概述2. BWP频域位置3. 初始与专用BWP4. 默认BWP5. 切换BWP 1. 概述 在LTE的设计中&#xff0c;默认所有终端均能处理最大2…

计算机毕业设计SpringBoot+Vue.js智能无人仓库管理系统(源码+文档+PPT+讲解)

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

qt-C++笔记之QToolButton和QPushButton的区别

qt-C笔记之QToolButton和QPushButton的区别 code review! 文章目录 qt-C笔记之QToolButton和QPushButton的区别1.运行2.main.cpp3.main.pro 1.运行 QToolButton 适用于工具栏或需要较紧凑、图标化显示的场合。通过 setAutoRaise(true) 与 setToolButtonStyle(Qt::ToolButtonTe…

[含文档+PPT+源码等]精品基于Python实现的vue3+Django计算机课程资源平台

基于Python实现的Vue3Django计算机课程资源平台的背景&#xff0c;可以从以下几个方面进行阐述&#xff1a; 一、教育行业发展背景 1. 教育资源数字化趋势 随着信息技术的快速发展&#xff0c;教育资源的数字化已成为不可逆转的趋势。计算机课程资源作为教育领域的重要组成部…

项目准备(flask+pyhon+MachineLearning)- 3

目录 1.商品信息 2. 商品销售预测 2.1 机器学习 2.2 预测功能 3. 模型评估 1.商品信息 app.route(/products) def products():"""商品分析页面"""data load_data()# 计算当前期间和上期间current_period data[data[成交时间] > data[成…

老牌工具,16年依然抗打!

在电脑还没普及、操作系统为Windows XP/7的时代&#xff0c;多媒体文件的转换操作常常面临格式不兼容的问题。这时一款名为格式工厂的软件成为了众多用户的首选工具。格式工厂以其简洁易用的界面和强大的功能&#xff0c;轻松地进行各种文件格式的转换。成为很多修小伙伴的喜爱…

LM studio 加载ollama的模型

1.LM 下载&#xff1a; https://lmstudio.ai/ 2.ollama下载&#xff1a; https://ollama.com/download 3.打开ollama&#xff0c;下载deepseek-r1。 本机设备资源有限&#xff0c;选择7B的&#xff0c;执行ollama run deepseek-r1:7b 4.windows chocolatey下载&#xff1a; P…

Linux内核以太网驱动分析

1.网络接口卡接收和发送数据在Linux内核中的处理流程如下&#xff1a; 1. 网络接口卡&#xff08;Network Interface Card, NIC&#xff09; 作用&#xff1a;负责物理层的数据传输&#xff0c;将数据包从网络介质&#xff08;如以太网线&#xff09;读取到内存中&#xff0c;或…

unity中使用spine详解

一.Spine概述 Spine 是一款针对游戏开发的 2D 骨骼动画编辑工具。 Spine 旨在提供更高效和简洁 的工作流程&#xff0c;以创建游戏所需的动画。 Spine原理&#xff1a;将一个模型&#xff0c;根据动画的需求分成一些骨骼&#xff0c;一个骨骼对应一张贴图&#xff0c;控制骨骼…

【计网】计算机网络概述

第一章 计算机网络概述 1.2 因特网概述1.2.1 网络、互联网和因特网1.2.2 因特网发展的三个阶段1.2.3 因特网的标准化工作1.2.4 因特网的组成 1.3 三种交换方式1.3.1 电路交换1.3.2 分组交换1.3.3 报文交换1.3.4 三种交换的对比 1.4 计网的定义与分类1.4.1 定义1.4.2 分类 1.5 计…

基于Linux系统的物联网智能终端

背景 产品研发和项目研发有什么区别&#xff1f;一个令人发指的问题&#xff0c;刚开始工作时项目开发居多&#xff0c;认为项目开发和产品开发区别不大&#xff0c;待后来随着自身能力的提升&#xff0c;逐步感到要开发一个好产品还是比较难的&#xff0c;我认为项目开发的目的…

[密码学实战]Java实现SM4加解密(ecb,cbc)及工具验证

前言 在现代信息安全领域,数据加密技术是保障数据安全的核心手段之一。SM4作为中国国家密码管理局发布的对称加密算法,因其高效性和安全性,广泛应用于金融、政务、通信等领域。本文将详细介绍如何使用Java实现SM4的加解密操作,并深入探讨SM4的几种常见加密模式及其原理。 …

StableDiffusion本地部署 3 整合包猜想

本地部署和整合包制作猜测 文章目录 本地部署和整合包制作猜测官方部署第一种第二种 StabilityMatrix下载整合包制作流程猜测 写了这么多python打包和本地部署的文章&#xff0c;目的是向做一个小整合包出来&#xff0c;不要求有图形界面&#xff0c;只是希望一键就能运行。 但…