前段时间写了关于CompletableFuture的使用博客,CompletableFuture使用方法详细说明CompletableFuture的thenCompose使用具体说明

但在实际中使用的时候发现,CompletableFuture开启的线程和当前事务是脱离开的,也就是当前上下文的事务和CompletableFuture中执行的事务是两个事务,谁也不影响谁,这样就会出现业务上的错乱。

例如:当前上下文执行失败,但是CompletableFuture中的执行成功了,而上下文因为失败进行了事务回滚,此时CompletableFuture中的是不会回滚的,这并不是我们想要的,所以针对这种情况进行测试说明和如何避免。

1.准备

1.1.表结构

CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL COMMENT '名称',
`age` int(11) DEFAULT NULL COMMENT '年龄',
`address` varchar(50) DEFAULT NULL COMMENT '地址',
`tranmemo` varchar(100) DEFAULT NULL COMMENT '备注',
`createtime` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

插入几条测试数据:

insert into user values (null, 'zhangsan', 18, 'CHINA', '我是中国的张三', SYSDATE())
, (null, 'lisi', 28, 'CHINA', '我是中国的李四', SYSDATE())
, (null, 'wangwu', 38, 'CHINA', '我是中国的王五', SYSDATE())
, (null, 'tom', 5, 'USA', 'Tom and Jerry', SYSDATE())
, (null, 'jerry', 5, 'USA', 'Tom and Jerry', SYSDATE());

查询user

1.2.线程池

线程池参考 ThreadPoolTaskExecutor线程池创建

2.复现事务问题

2.1.模拟业务正常情况

创建CompletableFutureTransactionalController, 代码如下:

/**
* 测试CompletableFuture事务问题
*
* @author jiangkd
* @date 2022/10/07 15:44:31
*/
@RequiredArgsConstructor
@RequestMapping("/cf")
@RestController
public class CompletableFutureTransactionalController { private final CompletableFutureTransactionalService CompletableFutureTransactionalService; /**
* 根据id修改User, 测试CompletableFuture事务问题
*
* @param userId User主键值
*/
@PostMapping("/test/transactional")
public String updateUser(@RequestParam("userId") Integer userId) throws ExecutionException, InterruptedException {
//
CompletableFutureTransactionalService.updateUser(userId);
return String.valueOf(userId);
} }

创建CompletableFutureTransactionalService, 代码如下:

/**
* @author jiangkd
* @date 2022/10/07 16:08:44
*/
@RequiredArgsConstructor
@Slf4j
@Service
public class CompletableFutureTransactionalService { private final ThreadPoolTaskExecutor threadPoolTaskExecutor; private final UserDAO userDAO; /**
* 根据id修改User, 测试CompletableFuture事务问题
*
* @param userId User主键值
*/
@Transactional(rollbackFor = Exception.class)
public void updateUser(final Integer userId) throws ExecutionException, InterruptedException {
/*
创建CompletableFuture线程
*/
final CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
//
log.info("runAsync线程执行, 线程名:{}", Thread.currentThread().getName()); // 根据参数查询user对象
final User user = userDAO.oneById(userId);
// 修改age
user.setAge(100);
// 执行update
user.update();
}, threadPoolTaskExecutor); // 执行CompletableFuture, runAsync没有返回, 所以这里使用Void接收
final Void unused = completableFuture.get();
log.info("CompletableFuture线程执行, 更新userId:{}的age成功, 返回结果:{}", userId, unused); /*
再次查询userId对应的user对象
*/
final User user = userDAO.oneById(userId);
// 修改备注
user.setTranmemo("测试CompletableFuture事务问题");
// 执行update
user.update();
log.info("当前上下文, 更新userId:{}的tranmemo成功~", userId); }
}

可以看出,这里的Service使用CompletableFuture开启线程更新了userId对应的age,然后上下文又更新了userId对应的tranmemo,这时候如果没有报错,那么正常的结果就是最后userId对应的age和tranmemo都被修改了。

注意: 这时候Service的方法updateUser上已经添加了事务注解 @Transactional(rollbackFor = Exception.class)

执行代码,结果如下:



对于返回结果,我在项目中实现了ResponseBodyAdvice进行统一返回格式,这里不多说,只要在乎结果执行成功了即可。

数据库查看user表:



很明显,userId为1对应的数据中,age和tranmemo都成功的修改成功了。

2.2.模拟异常情况 -- CompletableFuture执行异常

现在我们假设CompletableFuture中的线程在update之后发生了异常,小小的修改一下代码。

修改后的代码如下:



执行,结果如下:



可以看到,结果执行失败了,异常信息就是我们添加的 int i = 1/0 导致的,现在我们看看数据库中的结果



no~,age发生了改变, 但是tranmemo却没有被修改,这说明啥?是的,CompletableFuture中的线程执行失败后(其实就是 final Void unused = completableFuture.get() 这一行代码的执行失败了,失败后因为没有对异常的处捕获处理,所以就执行失败抛出了异常),后面的上下文代码没有执行了。

但是CompletableFuture中的update却依然生效,这显然不是我们想要的。

这里补充一点,如果CompletableFuture的runAsync方法执行后我们还使用了exceptionally, 结果就会有差异了,我们这里可以试一试。

添加exceptionally后的代码如下:



让我们看看现在执行后的结果:



数据库结果:

看到了嘛!!!age和tranmemo都执行成功了!!!为什么呢?

其实很简单,就是因为添加的exceptionally导致的,还是开头说的,看一看 CompletableFuture使用方法详细说明 后或许你就会明白了(如果你还是不明白,留言就行)。

现在我们继续往下走。

2.3.模拟异常情况 -- 上下文执行异常

现在我们继续假设CompletableFuture中的线程执行成功,但是上下文执行失败的情况。

代码修改后, 如下:



取消了CompletableFuture中的模拟异常,范围在上下文的最后添加模拟异常。

执行结果如下:

执行失败了,这是上下文的异常报出来的,我们再看一下数据库结果:



age被修改了,但是tranmemo没有变化,因为上下文抛出了异常,@transactional起了作用,事务发生了回滚。

进而说明,CompletableFuture中的事务和上下文的事务是分离的,上下文发生回滚的时候,无法回滚CompletableFuture中的修改内容。

显然,这种情况也不是我们想要的。

3.问题解决

针对刚才我们进行的测试,可以总结,无论CompletableFuture还是上下文,只要发生了异常,都存在结果不对的可能,而我们想要的是,无论谁存在问题,结果会回滚。

如何解决问题呢?

那就是手动开启CompletableFuture中的事务,手动回滚!!!

其实可以发现,无论CompletableFuture还是上下文发生异常,我们最终需要控制的,都是CompletableFuture的事务。

3.1.手动回滚异常

好了,现在该来解决刚才出现的问题了,不多说,直接上代码:



代码也贴一遍

@RequiredArgsConstructor
@Slf4j
@Service
public class CompletableFutureTransactionalService { private final ThreadPoolTaskExecutor threadPoolTaskExecutor; private final UserDAO userDAO; private final DataSourceTransactionManager dataSourceTransactionManager; /**
* 根据id修改User, 测试CompletableFuture事务问题
*
* @param userId User主键值
*/
@Transactional(rollbackFor = Exception.class)
public void updateUser(final Integer userId) throws ExecutionException, InterruptedException {
/*
创建CompletableFuture线程
*/
final CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
//
log.info("runAsync线程执行, 线程名:{}", Thread.currentThread().getName());
/*
手动开启事务
*/
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// 事物隔离级别,开启新事务,这样会比较安全些。
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
// 获得事务状态
TransactionStatus status = dataSourceTransactionManager.getTransaction(def); try {
// 根据参数查询user对象
final User user = userDAO.oneById(userId);
// 修改age
user.setAge(666);
// 执行update
user.update(); /*
模拟CompletableFuture异常
*/
int i = 1 / 0; } catch (Exception e) {
// 事务回滚
dataSourceTransactionManager.rollback(status);
throw new RuntimeException("runAsync线程执行失败", e);
} }, threadPoolTaskExecutor); // 执行CompletableFuture, runAsync没有返回, 所以这里使用Void接收
final Void unused = completableFuture.get();
log.info("CompletableFuture线程执行, 更新userId:{}的age成功, 返回结果:{}", userId, unused); /*
再次查询userId对应的user对象
*/
final User user = userDAO.oneById(userId);
// 修改备注
user.setTranmemo("222222");
// 执行update
user.update(); log.info("当前上下文, 更新userId:{}的tranmemo成功~", userId); }
}

我们再次执行:



注意,最然这里执行失败了,但是因为我们在catch中手动抛出了异常,如果不手动抛出异常,上下文就会继续执行,最后执行完修改了tranmemo,这是不对的,因为虽然执行成功了,但实际CompletableFuture是执行失败的(我们回滚了事务)。

再看数据库表:



好了,age和tranmemo都没有改变。

其实万变不离其宗,无论实际业务是怎么样,只要出现这种情况,需要手动控制事务,就这么处理即可(不是百分百这么些,根据实际需要)。

CompletableFuture事务问题的更多相关文章

  1. 006-优化web请求二-应用缓存、异步调用【Future、ListenableFuture、CompletableFuture】、ETag、WebSocket【SockJS、Stomp】

    四.应用缓存 使用spring应用缓存.使用方式:使用@EnableCache注解激活Spring的缓存功能,需要创建一个CacheManager来处理缓存.如使用一个内存缓存示例 package c ...

  2. 【RocketMQ】事务的实现原理

    事务的使用 RocketMQ事务的使用场景 单体架构下的事务 在单体系统的开发过程中,假如某个场景下需要对数据库的多张表进行操作,为了保证数据的一致性,一般会使用事务,将所有的操作全部提交或者在出错的 ...

  3. Spring基于AOP的事务管理

                                  Spring基于AOP的事务管理 事务 事务是一系列动作,这一系列动作综合在一起组成一个完整的工作单元,如果有任何一个动作执行失败,那么事务 ...

  4. SQLServer事务同步下如何收缩日志

    事务同步是SQLServer做读写分离的一种常用的方式. 随着业务数据的不断增长,数据库积攒了大量的日志,为了腾出硬盘空间,需要对数据库日志进行清理 订阅数据库的日志清理 因为订阅数据库所有的数据都来 ...

  5. 事务日志已满,原因为“ACTIVE_TRANSACTION”

    汇总篇:http://www.cnblogs.com/dunitian/p/4822808.html#tsql 异常处理汇总-数据库系列  http://www.cnblogs.com/dunitia ...

  6. Mysql事务探索及其在Django中的实践(二)

    继上一篇<Mysql事务探索及其在Django中的实践(一)>交代完问题的背景和Mysql事务基础后,这一篇主要想介绍一下事务在Django中的使用以及实际应用给我们带来的效率提升. 首先 ...

  7. Mysql事务探索及其在Django中的实践(一)

    前言 很早就有想开始写博客的想法,一方面是对自己近期所学知识的一些总结.沉淀,方便以后对过去的知识进行梳理.追溯,一方面也希望能通过博客来认识更多相同技术圈的朋友.所幸近期通过了博客园的申请,那么今天 ...

  8. CRL快速开发框架系列教程七(使用事务)

    本系列目录 CRL快速开发框架系列教程一(Code First数据表不需再关心) CRL快速开发框架系列教程二(基于Lambda表达式查询) CRL快速开发框架系列教程三(更新数据) CRL快速开发框 ...

  9. 玩转spring boot——结合JPA事务

    接着上篇 一.准备工作 修改pom.xml文件 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi=&q ...

  10. MySQL 系列(三)你不知道的 视图、触发器、存储过程、函数、事务、索引、语句

    第一篇:MySQL 系列(一) 生产标准线上环境安装配置案例及棘手问题解决 第二篇:MySQL 系列(二) 你不知道的数据库操作 第三篇:MySQL 系列(三)你不知道的 视图.触发器.存储过程.函数 ...

随机推荐

  1. Node.js学习笔记-----day05 (使用MongonDB重写学生信息管理案例)

    认真学习,认真记录,每天都要有进步呀!!! 加油叭!!! 使用MongonDB重写学生信息管理案例 1. 显示首页 先在crud-demo目录下安装mongoose 改写student.js var ...

  2. Nginx09 http的keepalive及在nginx的配置使用

    1 为什么要有Connection: keep-alive? 在早期的HTTP/1.0中,每次http请求都要创建一个连接,而创建连接的过程需要消耗资源和时间,为了减少资源消耗,缩短响应时间,就需要重 ...

  3. 有效的字母异位词&两个数组的交集& 快乐数& 两数之和

    一.有效的字母异位词 242. 有效的字母异位词 1.方法概述 因为本题中字符串只包含小写字母,就可以定义一个数组来记录字符串中字符出现的次数.遍历第一个字符串,用charAt拿到对应的小写字母然后减 ...

  4. 【开发宝典】Java并发系列教程(四)

    作者:京东零售 刘跃明 Monitor概念 Java对象的内存布局 对象除了我们自定义的一些属性外,还有其它数据,在内存中可以分为三个区域:对象头.实例数据.对齐填充,这三个区域组成起来才是一个完整的 ...

  5. Vue框架:9,Vue3的用法,setup函数,ref和reactive,计算属性和监听属性,生命周期,toRefs,script setup的作用和lang,Vue后台管理模板

    目录 前端开发之Vue框架 一.Vue3 1.Vue3创建项目 2.setup函数 3.ref和reactive 4.计算属性和监听属性 5.生命周期 6.toRefs 7.script setup的 ...

  6. 题解 [SCOI2005]王室联邦

    之前树分块也只是听说,今天亲手学了一下(?)( 首先你会发现这个 \(B\) 和 \(3B\) 的约束就很迷(我也不知道为什么搞这种奇怪的约束(悲)),学了才知道... 所以这题的分块方法好像叫&qu ...

  7. Vue学习笔记之Vue基础语法

    1. 概述 Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据.所有 Vue.js 的模板都是合法的 HTML,所以能被遵循规范的浏览器和 H ...

  8. 云原生数据库TDSQL-C 容灾的实践和探索

    云原生数据库TDSQL-C作为腾讯云架构平台部核心数据库产品之一,致力于为云上ToB用户和公司自研业务提供集高性能.低成本.大存储.低延迟.秒级扩缩容.极速回档.Serverless化七大特性于一体的 ...

  9. 8. fitBounds(用了这个你就不用在设置zoom, minZoom, maxZoom, center)

    1 <!DOCTYPE html> 2 <html lang="zh"> 3 <head> 4 <meta charset="U ...

  10. 2.4 在DispatcherServlet的service方法中,通过ServletPath获取对应的Controller对象

    @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws Se ...