源码角度分析多线程并发情况下数据异常回滚方案

news2025/1/11 2:28:46

一、 多线程并发情况下数据异常回滚解决方案

在需要多个没有前后顺序的数据操作情况下,一般我们可以选择使用并发的形式去操作,以提高处理的速度,但并发情况下,我们使用 @Transactional 还能解决事务回滚问题吗。

例如有下面表结构:

CREATE TABLE `test` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `thread_name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

假如需要进行两个写操作,并且写没有先后顺序之分,我们可以开个线程并发去写,这里以 JdbcTemplate 操作为例,使用其他DB工具也是一样的效果,例如:

@Service
public class TestService {

    @Resource
    JdbcTemplate jdbcTemplate;

    @Transactional(rollbackFor = Exception.class)
    public void test() {
        // 放入子线程
        CompletableFuture.runAsync(() -> {
            jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                    , new Object[]{LocalDateTime.now().toString(), Thread.currentThread().getName()});
        });
        // ....其他操作...
        jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                , new Object[]{LocalDateTime.now().toString(), Thread.currentThread().getName()});
        // ....其他操作...
    }
}

在这里插入图片描述

数据库成功写入了两条数据,假如在做其他操作时,发生了异常:

@Service
public class TestService {

    @Resource
    JdbcTemplate jdbcTemplate;

    @Transactional(rollbackFor = Exception.class)
    public void test() {
        // 放入子线程
        CompletableFuture.runAsync(() -> {
            jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                    , new Object[]{LocalDateTime.now().toString(), Thread.currentThread().getName()});
        });
        // ....其他操作...
        jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                , new Object[]{LocalDateTime.now().toString(), Thread.currentThread().getName()});
        // ....其他操作...
        int a = 1 / 0;
    }
}

运行后可以看到已经抛出了异常:
在这里插入图片描述

查看数据库:

在这里插入图片描述

发现还是写入了一条数据,线程中的操作并没有回滚,但主线程的回滚了,既然一个回滚一个没有回滚肯定用的不是同一个数据库连接,这里源码看下 JdbcTemplate 从哪里获取的数据库连接:

进到JdbcTemplateupdate(String sql, @Nullable Object... args) 方法中:
在这里插入图片描述
调用了当前类的 update(String sql, @Nullable PreparedStatementSetter pss) 方法中,最终调用的是当前类的 update(final PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss) 方法:
在这里插入图片描述

这里主要使用了 execute(StatementCallback<T> action) 方法,进到该方法中:

在这里插入图片描述

这里可以看出通过 DataSourceUtils.getConnection 方法获取数据库连接,进到该方法中:

在这里插入图片描述

这里看到 TransactionSynchronizationManager 是不是有点熟悉,在本专栏前面讲解@Transactional 声明式事务执行源码分析时,其中开启事务的逻辑中,就是使用 TransactionSynchronizationManager 获取的数据库连接,如果对这部分还不了解,可以看下下面这篇文章:

SpringTx 源码解析 - @Transactional 声明式事务执行原理

其实在String生态中,获取数据库连接基本都默认使用了 TransactionSynchronizationManager

这里也来看下当 @Transactional 注解情况下开启事务时获取连接的逻辑,在DataSourceTransactionManager 下的 doGetTransaction 方法下:

在这里插入图片描述

可以看到这里同样也是使用的 TransactionSynchronizationManager 获取连接。

下面看下TransactionSynchronizationManager 都做了啥,进到 getResource 方法:

在这里插入图片描述

这里又触发了 doGetResource 方法,进入到该方法下:

在这里插入图片描述

这里明显从 resources 中获取的,看下 resources 到底是个啥:

在这里插入图片描述

是一个 ThreadLocal ,现在是不是就明白了,在没有多线程的情况下,开启事务时就将拿到的连接放到了当前的 ThreadLocal 中,后面其他组件执行数据操作,同样先从ThreadLocal 中取连接,这样都在一个连接中操作,自然也可以进行回滚,由于上面我们是单独开启了线程,线程中的操作尝试获取 ThreadLocal中的连接,但获取不到,所以只能获取一个新的连接操作,导致了声明事务时的连接和实际操作时的连接不一致,从而无法进行回滚。

现在找到了问题的原因我们怎么解决呢?

既然是因为 ThreadLocal 导致的连接不同,那我们在开启线程时,就给它补充确实的信息,获取连接是用的 TransactionSynchronizationManager ,那添加同样也用 TransactionSynchronizationManager,通过观察 TransactionSynchronizationManagerApi,获取连接句柄可以使用 :

ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);

其中key就是当前数据源,绑定句柄可以使用:

 TransactionSynchronizationManager.bindResource(dataSource, conHolder);

移除句柄可以使用 :

TransactionSynchronizationManager.unbindResource(dataSource);

下面对前面的程序进行改造:

@Service
public class TestService {

    @Resource
    JdbcTemplate jdbcTemplate;

    @Resource
    DataSource dataSource;

    @Transactional(rollbackFor = Exception.class)
    public void test() {
        // 获取当前线程的句柄
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        // 放入子线程
        CompletableFuture.runAsync(() -> {
            // 子线程绑定
            TransactionSynchronizationManager.bindResource(dataSource, conHolder);
            jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                    , new Object[]{LocalDateTime.now().toString(), Thread.currentThread().getName()});
            // 解绑
            TransactionSynchronizationManager.unbindResource(dataSource);
        });
        // ....其他操作...
        jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                , new Object[]{LocalDateTime.now().toString(), Thread.currentThread().getName()});
        // ....其他操作...
        int a = 1 / 0;
    }
}

再次运行:

在这里插入图片描述

已经出现异常,查看数据库:

在这里插入图片描述

数据成功回滚了!

假入异常是出现在子线程的还可以回滚吗,下面开始实验一下:

@Service
public class TestService {

    @Resource
    JdbcTemplate jdbcTemplate;

    @Resource
    DataSource dataSource;

    @Transactional(rollbackFor = Exception.class)
    public void test() {
        // 获取当前线程的句柄
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        // 放入子线程
        CompletableFuture.runAsync(() -> {
            // 子线程绑定
            TransactionSynchronizationManager.bindResource(dataSource, conHolder);
            jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                    , new Object[]{LocalDateTime.now().toString(), Thread.currentThread().getName()});
            int a = 1 / 0;
            // 解绑
            TransactionSynchronizationManager.unbindResource(dataSource);
        });
        // ....其他操作...
        jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                , new Object[]{LocalDateTime.now().toString(), Thread.currentThread().getName()});
        // ....其他操作...
    }
}

运行后,查看数据:

在这里插入图片描述
发现没有出现回滚现象,这是因为异常在子线程的 Runnable 中,父线程没有感知到异常,怎么让父线程感知呢,我们可以加个在数据处理最后加个 join ,如果再出现异常就抛到父线程了:

@Service
public class TestService {

    @Resource
    JdbcTemplate jdbcTemplate;

    @Resource
    DataSource dataSource;

    @Transactional(rollbackFor = Exception.class)
    public void test() {
        // 获取当前线程的句柄
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        // 放入子线程
        CompletableFuture future = CompletableFuture.runAsync(() -> {
            // 子线程绑定
            TransactionSynchronizationManager.bindResource(dataSource, conHolder);
            jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                    , new Object[]{LocalDateTime.now().toString(), Thread.currentThread().getName()});
            int a = 1 / 0;
            // 解绑
            TransactionSynchronizationManager.unbindResource(dataSource);
        });
        // ....其他操作...
        jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                , new Object[]{LocalDateTime.now().toString(), Thread.currentThread().getName()});
        // ....其他操作...
        future.join();
    }
}

运行后,可以看到异常已经抛出来了:

在这里插入图片描述

查看数据库:

在这里插入图片描述

数据也成功回滚了。

二、延伸:MVC 子线程获取 Request 信息

看完上面事务的过程,同理在 MVC 中,加入原本是在主线程跑的,后面有需求需要放在子线程中优化,但是其中有从 ThreadLocal 中获取 Request 信息,此时就可以和上面一个做法解决问题,如:

@RestController
@RequestMapping("/test3")
public class RequestController {

    @GetMapping("/test")
    public void test(){
        // 获取句柄
        ServletRequestAttributes att = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        CompletableFuture.runAsync(()->{
            // 绑定
            RequestContextHolder.setRequestAttributes(att);

            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                    .getRequestAttributes();

            HttpServletRequest request = attributes.getRequest();
            System.out.println(request.getHeader("token"));
            // 解绑
            RequestContextHolder.resetRequestAttributes();
        }).join();
    }
}

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

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

相关文章

Go语言并发

Go语言并发学习目标 出色的并发性是Go语言的特色之一 • 理解并发与并行• 理解进程和线程• 掌握Go语言中的Goroutine和channel• 掌握select分支语句• 掌握sync包的应用 并发与并行 并发与并行的概念这里不再赘述, 可以看看之前java版写的并发实践; 进程和线程 程序、进程…

C语言3:根据身份证号输出生年月日和性别

18位身份证号码第7到10位为出生年份(四位数)&#xff0c;第11到12位为出生月份&#xff0c;第13 到14位代表出生日期&#xff0c;第17位代表性别&#xff0c;奇数为男&#xff0c;偶数为女。 用户输入一个合法的身份证号&#xff0c;请输出用户的出生年月日和性别。(不要求较验…

Java数据结构之第十三章、字符串常量池

目录 一、创建对象的思考 二、字符串常量池(StringTable) 三、再谈String对象创建 一、创建对象的思考 下面两种创建String对象的方式相同吗&#xff1f; public static void main(String[] args) {String s1 "hello";String s2 "hello";String s3 …

C# | 线性回归算法的实现,只需采集少量数据点,即可拟合整个数据集

C#线性回归算法的实现 文章目录 C#线性回归算法的实现前言示例代码实现思路测试结果结束语 前言 什么是线性回归呢&#xff1f; 简单来说&#xff0c;线性回归是一种用于建立两个变量之间线性关系的统计方法。在我们的软件开发中&#xff0c;线性回归可以应用于数据分析、预测和…

每日一博 - 对称加密算法 vs 非对称加密算法

文章目录 概述一、对称加密算法常见的对称加密算法优点&#xff1a;缺点&#xff1a;Code 二、非对称加密算法常见的非对称加密算法优点&#xff1a;缺点&#xff1a;Code 概述 在信息安全领域中&#xff0c;加密算法是保护数据安全的重要手段。 加密算法可以分为多种类型&am…

【Linux】线程互斥 与同步

文章目录 1. 背景概念多个线程对全局变量做-- 操作 2. 证明全局变量做修改时&#xff0c;在多线程并发访问会出问题3. 锁的使用pthread_mutex_initpthread_metux_destroypthread_mutex_lock 与 pthread_mutex_unlock具体操作实现设置为全局锁 设置为局部锁 4. 互斥锁细节问题5.…

哈夫曼树(Huffman)【数据结构】

目录 ​编辑 一、基本概念 二、哈夫曼树的构造算法 三、哈夫曼编码 假如<60分的同学占5%&#xff0c;60到70分的占15%…… 这里的百分数就是权。 此时&#xff0c;效率最高&#xff08;判断次数最少&#xff09;的树就是哈夫曼树。 一、基本概念 权&#xff08;we…

Zabbix4.0 自动发现TCP端口并监控

java端口很多&#xff0c;每台机器上端口不固定&#xff0c;考虑给机器配置组不同的组挂载模版&#xff0c;相对繁琐。直接使用同一个脚本自动获取机器上java相关的端口&#xff0c;推送到zabbix-server。有服务端口挂了自动推送告警 一、zabbix-agent配置过程 1、用户自定义参…

Apache Doris :Rollup 物化视图

整理了一下目前开启虚拟机需要用到的程序, 包括MySQL,Hadoop,Linux, hive,Doris 3.5 Rollup ROLLUP 在多维分析中是“上卷”的意思&#xff0c;即将数据按某种指定的粒度进行进一步聚合。 1.求每个城市的每个用户的每天的总销售额 select user_id,city,date&#xff0c; sum(…

树的简单介绍

目录 树的概念 ​ 树的相关概念 树的表示 二叉树的概念 特殊的二叉树 二叉树的存储结构 总结 树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#…

Diffie-Hellman密钥交换协议(Diffie-Hellman Key Exchange,简称DHKE)

文章目录 Diffie-Hellman密钥交换协议&#xff08;Diffie-Hellman Key Exchange&#xff0c;简称DHKE&#xff09;一、密码学相关的数学基础1. 素数&#xff08;质数&#xff09;2. 模运算3. 费马小定理4. 对数5. 离散对数6. 椭圆曲线常见椭圆曲线1. NIST系列曲线secp256k1 2. …

Django实现人脸识别登录

Django实现人脸识别登录 Demo示例下载 1、账号密码登录 2、人脸识别登录 3、注册 4、更改密码 5、示例网站 点我跳转 一、流程说明 1、注册页面:前端打开摄像头,拍照,点击确定后上传图像 2、后端获取到图像,先通过face_recognition第三方库识别是否能够获取到人脸特征…

开闭原则正确姿势, 使用AOP优雅的记录日志, 非常的哇塞

&#x1f473;我亲爱的各位大佬们好&#x1f618;&#x1f618;&#x1f618; ♨️本篇文章记录的为 JDK8 新特性 Stream API 进阶 相关内容&#xff0c;适合在学Java的小白,帮助新手快速上手,也适合复习中&#xff0c;面试中的大佬&#x1f649;&#x1f649;&#x1f649;。 …

使用腾讯云服务器快速搭建网站教程

已经有了腾讯云服务器如何搭建网站&#xff1f;腾讯云服务器网以腾讯云服务器&#xff0c;借助宝塔面板搭建Web环境&#xff0c;然后使用WordPress博客程序搭建网站&#xff0c;大致分为三步&#xff0c;首先购买腾讯云服务器&#xff0c;然后在腾讯云服务器上部署宝塔面板&…

Vue + Vite 构建 自己的ChartGPT 项目

前期回顾 两分钟学会 制作自己的浏览器 —— 并将 ChatGPT 接入_彩色之外的博客-CSDN博客自定义浏览器&#xff0c;并集合ChatGPT&#xff0c;源码已公开https://blog.csdn.net/m0_57904695/article/details/130467253?spm1001.2014.3001.5501 目录 效果图 代码步骤&am…

【Linux】软件包管理器/编辑器/yum是应用商店?/vim编辑器什么?

本文思维导图&#xff1a; 文章目录 Linux软件安装关于Linux的软件生态 1.Linux软件包管理器&#xff1a;yum到底是什么关于yum指令&#xff1a;关于yum源 2. rzsz指令1. Linux编辑器——vim编辑器vim编辑器的三种主要模式vim编辑器命令模式常用快捷键&#xff1a;vim操作总结…

spring(事务管理)

事物可以看做是由对数据库若干操作组成的一个单元 事务的作用就是为了保证用户的每一个操作都是可靠的&#xff0c;事务中的每一步操作都 必须成功执行&#xff0c;只要有发生异常就回退到事务开始未进行操作的状态,这些操作 要么都完成&#xff0c;要么都取消&#xff0c;从而…

Linux:/dev/tty、/dev/tty0 和 /dev/console 之间的区别

在Linux操作系统中&#xff0c;/dev/tty、/dev/tty0和/dev/console是三个特殊的设备文件&#xff0c;它们在终端控制和输入/输出过程中扮演着重要的角色。尽管它们看起来很相似&#xff0c;但实际上它们之间存在一些重要的区别。本文将详细介绍这三个设备文件之间的区别以及它们…

浅谈如何fltk项目编译和实现显示中文

目录 一、编译 二、中文显示如何处理&#xff1a; 2.1在发文2天前突然发现&#xff0c;我这个界面显示英文出现问题了&#xff0c;开始我的搜索之旅&#xff0c;一些参考页面有碰到问题也可以看看&#xff1a; 2.2、 那就开始翻翻官方自带的例程吧&#xff0c;看看他如何显…

Join的连接原理

1. 连接简介 1.1 连接的本质 连接就是把各个表中的记录都取出来进行一次匹配&#xff0c;并把匹配后的组合发送给客户端。如果连接查询中的结果集中包含一个表中的每一条记录与另一个表中的每一条记录相互匹配的组合&#xff0c;那么这样的结果集就可以称为笛卡尔积。 1.2 连…