二、异步某线程失败时,主线程回滚所有异步线程的事务!想要保证事务,肯定是使用@Transactional来实现。现在的场景是导入若干个大的Excel文件数据,因为每个Excel导入的表不同,所以只要保证单Excel的事务即可。上文中,是使用异步批量读取并插入的方式实现的Excel文件入库。也就是说,1个主线程事务 + 若干...
使用java的completablefuture怎么回滚事务?
大家好,我是哪吒。
一、前情提要
在上一篇文章中,我们通过双异步的方式导入了10万行的Excel,有个小伙伴在评论区问我,如果保证事务呢,如果分批的话。
原始需求:读取一个10万行的Excel
通过串行读取Excel,单个Excel耗时191s。
优化1:使用双异步后,从191s优化到2s
优化2:使用双异步后,如何保证数据一致性?
通过Future获取异步返回值,再和Excel文件数据行进行比较,实现对数据准确性的判断!
优化3:获取双异步返回值时,如何保证主线程不阻塞?
Java8中引入了CompletableFuture,它实现了对Future的全面升级,可以通过回调的方式,获取异步线程返回值。
CompletableFuture的异步执行通过ForkJoinPool实现,它使用守护线程去执行任务。
ForkJoinPool在于可以充分利用多核CPU的优势,把一个任务拆分成多个小任务,把多个小任务放到多个CPU上并行执行,当多个小任务执行完毕后,再将其执行结果合并起来。
二、异步某线程失败时,主线程回滚所有异步线程的事务!
想要保证事务,肯定是使用@Transactional来实现。
现在的场景是导入若干个大的Excel文件数据,因为每个Excel导入的表不同,所以只要保证单Excel的事务即可。
上文中,是使用异步批量读取并插入的方式实现的Excel文件入库。
也就是说,1个主线程事务 + 若干个子线程事务,我们想要保证单Excel的插入事务,所有异步子线程有任何一个报错,都要进行事务回滚,如果全部都没报错,则进行事务提交。
这个时候,有的小伙伴可能会想到,主线程加个@Transactional注解,所有子线程分别加@Transactional注解,就可以了吧?
但是,这样是不行的,子线程的异常只会回滚其自身的事务。
如果Excel中有10万条数据,一次插入4200条数据,最后一次插入3400条。如果其它线程都插入成功了,最后一个报错了,此时,数据库中还是会有96600条数据插入成功,与单Excel的事务需求不符。
通过代码模拟这种情况:
三、@Transactional注解
声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
简而言之,@Transactional注解在代码执行出错的时候能够进行事务的回滚。
1、@Transactional
使用@Transactional后,当程序发生RuntimeException运行时异常在没有使用try,catch进行捕获的时候,程序都会中止,当程序发生中止,则会触发数据库的回滚。
当使用了trycatch进行捕获到这个异常,假如在catch中加入了throw e抛出异常,则程序中止,数据库回滚。
加入在try catch中没有throw e 抛出异常,只是简单的打印异常,则异常被捕获未抛出异常去终止程序,在trycatch中的操作数据库语句插入失败,在trycatch上面和下面的数据库相关插入语句成功,也就是程序成功跑完,数据库不会发生回滚。
2、@Transactional(rollbackFor = Exception.class)
在@Transactional注解中如果不配置rollbackFor属性,那么事物只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事物在遇到非运行时异常时也回滚。
四、注解失效问题
1、@Transactional应用在非public修饰的方法上
事务拦截器在目标方法执行前后进行拦截,内部会调用方法来获取Transactional注解的事务配置信息,调用前会检查目标方法的修饰符是否为public,不是public则不会获取@Transactional的属性配置信息。
2、@Transactional注解属性rollbackFor设置错误
rollbackFor可以指定能够触发事务回滚的异常类型。
Spring默认抛出了未检查unchecked异常(继承自RuntimeException的异常)或者Error才回滚事务;其他异常不会触发回滚事务。
如果在事务中抛出其他类型的异常,但却期望Spring能够回滚事务,就需要指定rollbackFor属性。
3、同一个类中方法调用,导致@Transactional失效
开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。
那为啥会出现这种情况?其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。
在同一个类中调用异步方法,等于调用this本类的方法,没有走Spring生成的代理类,也就不会让他异步执行,@Transactional的原理也类似。
4、捕获异常
如果你手动的catch捕获这个异常并进行处理,事务管理器会认为当前事务应该正常commit,就会导致注解失效,如果非要捕获且不失效,就必须在代码块内throw new Exception抛出异常。
五、通过Future获取异步返回值,添加事务
1、添加事务(1)添加事务 + 不开启异步
如果入库异常,事务回滚成功
(2)添加事务 + 开启异步
回滚失败!
2、手动添加事务(1)添加事务 + 不开启异步
如果入库异常,事务回滚成功
(2)Future获取异步返回值,添加手动事务,异常回滚失败!
六、@async + @Transactional事务失效问题
回顾一下需求:异步某线程失败时,主线程回滚所有异步线程的事务!
是代码有问题,还是就是实现不了呢?
@Async和@Transactional注解都是通过Spring aop实现的,核心都是靠着关键的MethodInterceptor实现,@Async会给对应bean代理对象中放入一个AnnotationAsyncExecutionInterceptor拦截器,而@Transactional会给对应bean的代理对象中放入一个TransactionInterceptor拦截器。
Spring事务管理的传播机制是使用ThreadLocal实现的。因为ThreadLocal是线程私有的,所以Spring的事务传播机制是不能够跨线程的。
七、Spring的事务传播机制是不能够跨线程的
1、一个异步线程一个事务,然后根据结果统一提交/回滚?
2、核心代码
3、异步线程类
4、事务复制类
5、为何要用事务复制类?而最后提交和回滚的时候也没用它?
如何不加会怎么样?
在提交和回滚的时候,会出现异常:
八、总结
经过不懈的努力,终于解决了“异步某线程失败时,主线程回滚所有异步线程的事务!”这个看起来很简单的问题。
也是对双异步入库系列的一个完结。
通过添加事务,可以有效的控制Excel异步插入数据的准确性。
读取一个10万行的Excel的最佳解决方案是:2024-11-19