【多线程场景下事务失效问题如何处理?】

一.什么是多线程事务

多线程事务是指在多线程环境中处理事务的场景,其中多个线程可能同时执行数据库操作,并可能在同一事务中涉及多个资源或操作。这种情况下,需要确保事务的一致性、隔离性和完整性,以防止数据不一致或冲突。
关键点:

  • 并发性:多个线程可能同时访问和修改数据库,导致数据竞争。
  • 事务一致性:确保所有线程的操作要么全部成功(提交),要么在出现错误时全部失败(回滚)。
  • 隔离性:不同线程之间的事务应相互独立,避免相互干扰。

性能考虑:在多线程环境中,管理事务可能会影响性能,因此需要平衡一致性和系统效率。

注意:这里的多线程操作的是同一个数据源,非多个数据源,暂时不讨论分布式事务。

二.业务背景

多线程业务场景:我这里这是举例哈…假如我们新增完一个用户的时候, 异步给用户授权角色信息。
小伙伴们应该有了解1.8新特性CompletableFuture异步编排吧,具体怎么使用我这里不做深入讲了,api而已。 需要注意点就是要传入自定义线程池,底层默认使用的是 ForkJoinPool.commonPool() 作为其线程池,默认情况下,线程池的大小为可用处理器的数量(即 Runtime.getRuntime().availableProcessors()),在高负载情况下可能会导致性能问题。 需要传入自定义线程池~

	/**
     * 子线程插入角色信息出错,主线程正常。测试结果:用户信息没插入,角色信息插入了
     */
    @Transactional
    public void test1() throws ExecutionException, InterruptedException {
        // 插入用户信息
        jdbcTemplate.execute("insert into jinbiao_user values (3,'rise3','wang1234..','10086','小程序')");
        CompletableFuture.runAsync(()->{
             // 插入角色信息
            jdbcTemplate.execute("insert into jinbiao_role values (3,'admin')");
            
            // 模拟其他业务操作造成任务报错了--
            throw new RuntimeException("角色信息业务出错啦...");
        },tulingThreadPoolExecutor).join();  //.get()阻塞等待,则用户信息也不回滚
       // 处理用户其他逻辑等等....
  }
  
 /**
 * 主线程出错,子线程插入角色信息正常。测试结果:用户信息没插入,角色信息插入了
  */
 @Transactional
 public void test2() throws ExecutionException, InterruptedException {
      // 插入用户信息
      jdbcTemplate.execute("insert into jinbiao_user values (3,'rise3','wang1234..','10086','小程序')");
      CompletableFuture.runAsync(()->{
          // 插入角色信息
          jdbcTemplate.execute("insert into jinbiao_role values (3,'admin')");
      },tulingThreadPoolExecutor).get();  
      throw new RuntimeException("用户信息业务出错啦...");

FAQ:上面代码会存在什么问题?
答:先说结论再说原因,用户信息没插入,角色信息插入了。因为join()方法等待结果抛出的是CompletionExcetion属于运行时异常,所以我们的声明式事务(@Transactional)生效,用户信息是会回滚的。 runAsync里面的任务是非事务方法,所以角色信息会插入成功,不会回滚。
ps:用户信息都不存在,用户的角色信息确已经入库生效了,这怎么行呢。所以我们得想办法:

  • 如果子线程里面插入角色信息报错了,那么它要回滚,插入的用户信息也要回滚。
  • 如果主线程里面插入用户信息报错了,那么它要回滚,插入的角色信息也要回滚

如果计算抛出异常, join() 会抛出一个CompletionException(extends RuntimeException),是运行时异常。
如果计算抛出异常,get() 会抛出一个受检异常 ExecutionException(extends Exception),它包装了实际的异常(也可以通过 getCause() 获取)。 声明式事务(@Transactional)默认不会生效!get() 还会抛出 InterruptedException(extends Exception),如果当前线程在等待结果时被中断,需要显式处理ExecutionException 和 InterruptedException 异常。 属于非运行时异常!声明式事务(@Transactional)默认不会生效!
关于get()/join()阻塞等待获取结果异常测试,rollback指定异常回滚源码分析,@Transactional默认只能回滚运行时异常 以及 所有Error 的 实例。 想具体了解可以去看看我前面文章,有debug关键源码位置:【深入学习Spring声明式事务,测试失效场景及原因分析】

角色信息插入成功。
在这里插入图片描述
用户信息回滚了,未插入。
在这里插入图片描述

三.多线程事务解决方案

之前从其他博主那学过一种基于主事务 +指定几个子事务+结合切面。这种方案, 总体思想就是主事务跟子事务去维护一个共享的事务执行状态,只要有任务报错则把这个共享状态置为true。主线程先执行完则阻塞等待所有子线程执行完,子线程先执行完则阻塞等待主线程执行完,相互等待对方完成,最后根据这个共享状态判断是否需要回滚或者提交。 那位博主的代码很复杂,他demo其实有小问题的然后我改造后测试是ok的,怕给我的小伙伴们劝退了,不贴这种方式了hhh,讲点其他简单又好用的方式。

1. 子线程新开一个事务

让主线程和子线程不在同一个事务里,把线程里面的逻辑放到一个新的事物方法里面去,有异常则各自事物回滚各自的。

	@Autowired
    private RoleService roleService;
    
    @Transactional
    public void test1() throws ExecutionException, InterruptedException {
        // 插入用户信息
        jdbcTemplate.execute("insert into jinbiao_user values (3,'rise3','wang1234..','10086','小程序')");
        CompletableFuture.runAsync(()->{
            roleService.insertRole2();
        },tulingThreadPoolExecutor).join();  //.get()阻塞等待,则用户信息也不回滚
   }

// 角色相关的业务类
@Service
public class RoleService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional
    public void insertRole1() {
        jdbcTemplate.execute("insert into jinbiao_role values (1,'admin')");
    }

    @Transactional
    public void insertRole2() {
        jdbcTemplate.execute("insert into jinbiao_role values (2,'admin')");
        throw new RuntimeException("子线程事务方法出错啦...");
    }
}

测试结果:看到两条回滚信息:“Initiating transaction rollback”,数据都未入库。说明我们让主线程和子线程开启不同的事务是可行的。
在这里插入图片描述
如果是使用get阻塞获取结果呢? 上面提到使用get阻塞获取结果抛出的是ExecutionException(extends Exception) 或者InterruptedException(extends Exception)都是属于非运行时异常,如果我们没有指定rollbackFor异常,这里就需要我们手动try catch 抛出运行时异常了,如这样:

   @Transactional
    public void test4() throws ExecutionException, InterruptedException {
        // 插入用户信息
        jdbcTemplate.execute("insert into jinbiao_user values (3,'rise3','wang1234..','10086','小程序')");
        try {
            CompletableFuture.runAsync(()->{
                roleService.insertRole2();
            },tulingThreadPoolExecutor).get();  //.get()阻塞等待,则用户信息也不回滚
        }catch (Exception e){
            throw new RuntimeException("角色信息业务出错啦...");
        }
   }

测试结果:插入角色信息出错,用户信息、角色信息都一起回滚,控制台日志提示两条"Initiating transaction rollback",

2. 使用编程式事务

使用共享变量来标记是否有异常,并在两个线程中各自进行回滚,主线程使用TransactionTemplate开启一个事务,子线程也使用TransactionTemplate开启一个事务,并在执行过程中检查共享变量,决定是否回滚。如果共享变量被设置为异常状态,则在主线程中也触发回滚。。示例代码如下:

public void programmingTransaction() {
        transactionTemplate.execute(status -> {
            // 插入用户信息
            jdbcTemplate.execute("insert into jinbiao_user values (3,'rise3','wang1234..','10086','小程序')");
            CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
                try {
                    transactionTemplate.execute(innerStatus -> {
                        // 插入角色信息
                        jdbcTemplate.execute("insert into jinbiao_role values (3,'admin')");
                        // 模拟异常
                        throw new RuntimeException("角色信息业务出错啦...");
                    });
                } catch (Exception e) {
                    // 标记为有错误
                    needRollBack.set(true);
                }
            });
            // 等待子线程完成
            future.join();

            // 根据共享变量决定是否回滚
            if (needRollBack.get()) {
                // 设置主线程事务回滚
                status.setRollbackOnly();
            }
            return null;
        });
    }

测试结果:插入角色信息出错,用户信息、角色信息都一起回滚,控制台日志提示两条"Initiating transaction rollback"
在这里插入图片描述
使用编程式事务还有通过PlatformTransactionManager的方式,实现上与上面类似。只是PlatformTransactionManager来进行编程式事务使用上有区别,需要自己手动commit/rollback事务状态(TransactionStatus), 思想是一样的,不做多介绍啦。

当然还有很多种方式处理多线程事务问题,个人认为上面两种处理方案是比较简单的了,性能也是最优解了。

四.小结

算是最简单的方式来介绍多线程场景下事务失效问题处理方案了。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值