摘要
Spring Batch结合Hibenate(使用Hibernate Reader、Hibernate Writer)时,由于Hibernate Session的一些配置、约束,可能导致session异常、事务泄漏等问题。
问题
问题1
当Reader中读取的实体包含@OneToMany的关联关系(如LoanProduct实体中对应的repayParameter。实际出现问题的是另一个实体,但那个实体中的OneToMany关系已经被删掉了。因此用LoanProduct举例),并且在Processor中更新了该实体时,那么在writer中会抛出异常:
org.hibernate.HibernateException: Illegal attempt to associate a collection with two open sessions。
问题2
当在Reader中读取了一个实体(不包含@OneToMany关系,以免出现问题1的异常),然后在Processor中调用service,service中用PROPAGATION_REQUIRES_NEW配置开一个新事务,并在Processor中更新该实体时,即使抛出异常、事务回滚,该实体依然会被更新到数据库中。
分析
spring batch的事务传播机制
按SpringBatch的默认配置,其事务会如下传播。其中,代码列是指执行到该方法时,会有相关事务处理。
代码位置 | 事务配置 | 事务 | 边界 | session |
---|---|---|---|---|
SimpleJobRepository.getLastJobExecution | PROPAGATION_REQUIRES_NEW | new | - | - |
SimpleJobRepository.createJobExecution | PROPAGATION_REQUIRES_NEW | new | - | - |
SimpleJobRepository.update | PROPAGATION_REQUIRED | new | - | - |
SimpleJobRepository.getStepExecutionCount | PROPAGATION_REQUIRED | new | - | - |
SimpleJobRepository.getLastStepExecution | PROPAGATION_REQUIRED | new | - | - |
SimpleJobRepository.getStepExecutionCount | PROPAGATION_REQUIRED | new | - | - |
SimpleJobRepository.add | PROPAGATION_REQUIRED | new | - | - |
SimpleJobRepository.update | PROPAGATION_REQUIRED | new | - | - |
SimpleJobRepository.updateExecutionContext | PROPAGATION_REQUIRED | new | - | - |
null(从这里开始进入reader-processor-writer) | PROPAGATION_REQUIRED | new | 开始事务 | |
执行SqlQuery | - | - | - | openSession() |
reader.doRead | - | - | RepeatTemplate#isComplete后进入下一步 | current |
processor | - | - | - | current |
writer | - | - | 提交事务 | current |
SimpleJobRepository.updateExecutionContext | PROPAGATION_REQUIRED | new | - | - |
SimpleJobRepository.update | PROPAGATION_REQUIRED | new | - | - |
SimpleJobRepository.updateExecutionContext | PROPAGATION_REQUIRED | new | - | - |
SimpleJobRepository.update | PROPAGATION_REQUIRED | new | - | - |
代码列中的“null”,应该是在运行过程中动态生成的代码,因此日志中无法获取其类名。
需要注意的是,基本上对batch表的各类操作,虽然大部分配置是PROPAGATION_REQUIRED,但基本上都是“Creating new transaction”。这可以保证业务逻辑的事务不影响对batch表的更新操作。
另外,执行SqlQuery(即reader中配置的sql/hql)的方法是打开了一个全新的session,而没有使用当前事务上下文中的currentSession。这是因为这个SqlQuery会一次性把所有数据全部读取出来;而当前事务只会根据commit-internal配置的数量,处理其中一部分数据并进行提交,并关闭currentSession。因此SqlQuery操作必须使用一个独立的Session,并在所有数据都提交后才close。
问题1
问题1的原因是比较明确的。当执行SqlQuery读取数据时,SpringBatch开启了一个新的session,此时@OneToMany的数据会绑定在这个session上。而后在writer中处理更新和提交事务时,会再次尝试把它绑定到currentSession上。由于hibernate的约束(一个集合不能绑定到两个不同的session上),此时就会抛出异常。
问题2
问题2的原因实际与spring batch并没有关系,任何一个在已有事务环境中开启新事务/嵌套事务的地方都存在这种风险。
当父事务中读取了实体A,这个实体就会放入父事务的session缓存中。如果子事务中修改了实体A的某个字段,并尝试更新数据库,但更新失败、子事务发生回滚,此时虽然update语句执行失败,但实体A的字段值仍然被修改了。然后,随着父事务提交、父事务的session做flush操作,实体A再次被更新。由此就发生了事务泄漏。
问题2就是这样发生的。reader/processor/writer中有自己的事务,它调用的service中又开启了新的事务。当service中修改了reader所读取的实体的属性时,即使service的子事务回滚,writer中仍然会将实体属性的修改做一次更新。
解决方案
问题1
针对问题1,目前的方案是去掉了实体中@OneToMany的关联关系。暂时没有其它更好的方案。
问题2
针对问题2,当前解决方案是重写了writer类,覆盖了原有的flush整个session的操作。这样,即使实体中的字段已经更新过了,也不会再被flush到数据库中。
后续工作
整理一个spring batch的最佳实践/建议方案/代码模板。
小结
hibernate的session为我们做了很多工作。但是有些场景下,这些工作有些“多余”,甚至会影响正常逻辑。建议逐步转向myBatis。
参考
Transactions in Spring Batch – Part 1: The Basics
Transactions in Spring Batch – Part 2: Restart, cursor based reading and listeners