首页 > 其他分享 >CompletableFuture事务问题

CompletableFuture事务问题

时间:2022-10-07 15:44:46浏览次数:71  
标签:事务 userId 问题 线程 user CompletableFuture 执行 final

前段时间写了关于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都没有改变。

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

标签:事务,userId,问题,线程,user,CompletableFuture,执行,final
From: https://www.cnblogs.com/no-celery/p/16759846.html

相关文章

  • 关于blender/pmx/fbx/mmd/骨骼/材质等等等等的问题所有疑问和解决方法
    1.关于将pmx文件导入blender,进行fixmodel之后,导致材质混乱的问题ansR:blender版本问题,建议使用blender2.9.0(看一些人的教程,理论上2.8.7也可以),已知的是3.3肯定会有这个问题......
  • Spring事务(六)-只读事务
    @Transactional(readOnly=true)就可以把事务方法设置成只读事务。设置了只读事务,事务从开始到结束,将看不见其他事务所提交的数据。这在某种程度上解决了事务并发的问题。一......
  • 问题:执行时中文乱码
    CMD/BAT文件执行时中文乱码直接原因文件编码很多的文本编辑器会都是默认将文件保存为UTF-8编码,而cmd并不认。比如记事本Notepad、notepad++等。解决方法--ANSI保存......
  • MySQL基础--事务--2022年10月7日
    第一节  事务简介1、什么是事务2、注意:默认MySQL的事务是自动提交的,也就是说,当执行完一条DML语句时,MaSQL会立即隐式的提交事务。第二节  事务......
  • orioledb 对于top pg 问题的解决方法
    内容来自官方ppt,支持整理下,可以看出提升还是不少的问题  解决方法  说明官方那个ppt很值得查看,从官方的压测报告来看,提升是不少的,期待ga,后边测试下与citus集成的可能......
  • 通过openresty 解决遗留 webservice 接口安全问题
    技术一直在变革,老的技术一般都会成为现在的技术债,加上早期大家一般对于安全不是很重视(尤其是在内网环境的时候),尽管webservice是包含了ws-security安全指南的,但是很多时......
  • nginx proxy webservie 问题&实践
    webservice具有特殊性,因为wsdl文件是服务器端生成的(大部分,而且是动态的),所以我们直接使用nginx进行proxy会有问题实际上此问题比较常见,而且网上也有人碰到,可能因为时间......
  • LeetCode双堆问题
    FindMedianfromDataStreamLeetCode/力扣数组保存数据,add的时候直接在末尾插入,查找的时候,先排序,然后再算其中的结果vector<int>data;/**initializeyourdatastr......
  • mongo docker 内存问题
    mongodocker镜像对于cgroup的内存管理是有点问题的,所以推荐基于容器运行mongo的配置上wiredTigerCacheSizeGB的大小可以规避内存占用的问题(同时最好做好内存限制)服......
  • 一次nginx 请求真实ip 问题处理
    nginxngx_http_realip模块是比较重要的,我以前也大概说过,同时网上关于此模块的资料也不少,今天就碰到了一个获取真实ip的问题记录下参考业务模型  问题以前的配置,waf......