【多线程场景下事务失效问题如何处理?】
文章目录
- 一.什么是多线程事务
- 二.业务背景
- 三.多线程事务解决方案
- 1. 子线程新开一个事务
- 2. 使用编程式事务
- 四.小结
一.什么是多线程事务
多线程事务是指在多线程环境中处理事务的场景,其中多个线程可能同时执行数据库操作,并可能在同一事务中涉及多个资源或操作。这种情况下,需要确保事务的一致性、隔离性和完整性,以防止数据不一致或冲突。
关键点:
- 并发性:多个线程可能同时访问和修改数据库,导致数据竞争。
- 事务一致性:确保所有线程的操作要么全部成功(提交),要么在出现错误时全部失败(回滚)。
- 隔离性:不同线程之间的事务应相互独立,避免相互干扰。
性能考虑:在多线程环境中,管理事务可能会影响性能,因此需要平衡一致性和系统效率。
注意:这里的多线程操作的是同一个数据源,非多个数据源,暂时不讨论分布式事务。
二.业务背景
多线程业务场景:我这里这是举例哈…假如我们新增完一个用户的时候, 异步给用户授权角色信息。
小伙伴们应该有了解1.8新特性CompletableFuture异步编排吧,具体怎么使用我这里不做深入讲了,api而已。 需要注意点就是要传入自定义线程池,底层默认使用的是 ForkJoinPool.commonPool() 作为其线程池,默认情况下,线程池的大小为可用处理器的数量(即 Runtime.getRuntime().availableProcessors()),在高负载情况下可能会导致性能问题。 需要传入自定义线程池~
/*** 子线程插入角色信息出错,主线程正常。测试结果:用户信息没插入,角色信息插入了*/@Transactionalpublic 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()阻塞等待,则用户信息也不回滚// 处理用户其他逻辑等等....}/*** 主线程出错,子线程插入角色信息正常。测试结果:用户信息没插入,角色信息插入了*/@Transactionalpublic 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. 子线程新开一个事务
让主线程和子线程不在同一个事务里,把线程里面的逻辑放到一个新的事物方法里面去,有异常则各自事物回滚各自的。
todo:还要考虑主线程先报错了,子线程正常的情况。主线程需要通知子线程进行回滚,我们可以使用父子线程传一个布尔值,子线程判断这个布尔值为true,则抛异常~
@Autowiredprivate RoleService roleService;@Transactionalpublic 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 {@Autowiredprivate JdbcTemplate jdbcTemplate;@Transactionalpublic void insertRole1() {jdbcTemplate.execute("insert into jinbiao_role values (1,'admin')");}@Transactionalpublic 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 抛出运行时异常了,如这样:
@Transactionalpublic 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), 思想是一样的,不做多介绍啦。
当然还有很多种方式处理多线程事务问题,个人认为上面两种处理方案是比较简单的了,性能也是最优解了。
四.小结
算是最简单的方式来介绍多线程场景下事务失效问题处理方案了。