明天这篇文章跟大家分享一个实战中的 Bug 及解决方案和技术延长。
事件是这样的:经营人员反馈,通过 Excel 导入数据时,有一部分胜利了,有一部分未导入。初步猜想,是事务未失效导致的。
查看代码,发现导入局部曾经通过 @Transcational 注解进行事务管制了,为什么还会呈现事务不失效的问题呢?
上面咱们就进行具体的案例剖析,Let’s go!
事务不失效的代码
这里写一段简略的伪代码来演示展现一下事务不失效的代码:
@Transactional(rollbackFor = Exception.class)
public void batchInsert(List<Order> list) {list.parallelStream().forEach(order -> orderMapper.save(order));
}
逻辑很简略,遍历 list,而后批量插入 Order 数据到数据库。在该办法上应用 @Transactional 来申明出现异常时进行回滚。
但事实状况是,其中某一条数据执行异样时,事务并没有进行回滚。这到底是为什么呢?
上面一探到底。
JDK 8 的 Stream
下面代码中波及到了两个知识点:parallelStream 和 @Transactional,咱们先来铺垫一下 parallelStream 相干常识。
在 JDK8 中引入了 Stream API 的概念和实现,这里的 Stream 有别于 InputStream 和 OutputStream,Stream API 是解决对象流而不是字节流。
比方,咱们能够通过如下形式来基于 Stream 进行实现:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
numbers.stream().forEach(num->System.out.println(num));
输入:1 2 3 4 5 6 7 8 9
代码看起来不便清新多了。对于 Stream 的根本解决流程如下:
在这些 Stream API 中,还提供了一个并行处理的 API,也就是 parallelStream。它能够将工作拆分子工作,分发给多个处理器同时解决,之后合并。这样做的目标很显著是为了晋升解决效率。
parallelStream 的根本应用形式如下:
// 并行执行流
list.stream().parallel().filter(e -> e > 10).count()
针对上述代码,对应的流程如下:
而 parallelStream 会将流划分成多个子流,扩散到不同的 CPU 并行处理,而后合并处理结果。其中,parallelStream 默认是基于 ForkJoinPool.commonPool() 线程池来实现并行处理的。
通常状况下,咱们能够认为并行会比串行快,但还是有前提条件的:
- 处理器外围数量:并行处理外围数越多,解决效率越高;
- 解决数据量:解决数据量越大劣势越显著;
但并行处理也面临着一系列的问题,比方:资源竞争、死锁、线程切换、事务、可见性、线程平安等问题。
@Transactional 事务处理
下面理解了 parallelStream 的基本原理及个性之后,再来看看 @Transactional 的事务处理个性。
@Transactional 是 Spring 提供的基于注解的一种申明式事务形式,该注解只能使用到 public 的办法上。
基本原理 :当一个办法被 @Transactional 注解之后,Spring 会基于 AOP 在办法执行之前开启一个事务。当办法执行结束之后,依据办法是否报错,来决定回滚或提交事务。
在默认代理模式下,只有指标办法由内部办法调用时,能力被 Spring 的事务拦截器拦挡。所以,在同一个类中的两个办法间接调用,不会被 Spring 的事务拦截器拦挡。这是事务不失效的一个场景,但在上述案例中,并不存在这种状况。
Spring 在处理事务时,会从连接池中取得一个 jdbc connection,将连贯绑定到线程上(基于 ThreadLocal),那么同一个线程中用到的就是同一个 connection 了。具体实现在 DataSourceTransactionManager#doBegin 办法中。
Bug 综合剖析
在理解了 parallelStream 和 @Transactional 的相干常识之后,咱们会发现:parallelStream 解决时开启了多线程,而 @Transactional 在处理事务时会(基于 ThreadLocal)将连贯绑定到以后线程,因为 @Transactional 绑定治理的是主线程的事务,而 parallelStream 开启的新的线程与主线程无关。因而,事务也就有效了。
此时,将 parallelStream 改为一般的 stream,事务可失常回滚。这就提醒咱们,在应用基于 @Transactional 形式治理事务时,谨慎应用多线程解决。
问题拓展
尽管 parallelStream 带来了更高的性能,但也要辨别场景进行应用。即使是在不须要事务管理的状况下,如果 parallelStream 使用不当,也会造成同一时间对数据库发动大量申请等问题。
因而,在 stream 与 parallelStream 之间进行抉择时,还要思考几个问题:
- 是否须要并行?数据量比拟大,处理器外围数比拟多的状况下才会有性能晋升。
- 工作之间是否是独立的,是否会引起任何竞态条件?比方:是否共享变量。
- 执行后果是否取决于工作的调用程序?并行执行的程序是不确定的。
小结
本篇文章讲述的 Bug 尽管简略,但如果不理解 parallelStream 与 @Transactional 注解的个性,还是很难排查的。而且也让咱们意识到,尽管 Spring 通过 @Transactional 将事务管理进行了简化解决,但作为开发者,还是须要深刻理解一下它的根本运作原理。不然,在排查 bug 时,很容易抓瞎。
博主简介:《SpringBoot 技术底细》技术图书作者,热爱钻研技术,写技术干货文章。
公众号:「程序新视界」,博主的公众号,欢送关注~
技术交换:请分割博主微信号:zhuan2quan