摘要

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.getLastJobExecutionPROPAGATION_REQUIRES_NEWnew--
SimpleJobRepository.createJobExecutionPROPAGATION_REQUIRES_NEWnew--
SimpleJobRepository.updatePROPAGATION_REQUIREDnew--
SimpleJobRepository.getStepExecutionCountPROPAGATION_REQUIREDnew--
SimpleJobRepository.getLastStepExecutionPROPAGATION_REQUIREDnew--
SimpleJobRepository.getStepExecutionCountPROPAGATION_REQUIREDnew--
SimpleJobRepository.addPROPAGATION_REQUIREDnew--
SimpleJobRepository.updatePROPAGATION_REQUIREDnew--
SimpleJobRepository.updateExecutionContextPROPAGATION_REQUIREDnew--
null(从这里开始进入reader-processor-writer)PROPAGATION_REQUIREDnew开始事务 
执行SqlQuery---openSession()
reader.doRead--RepeatTemplate#isComplete后进入下一步current
processor---current
writer--提交事务current
SimpleJobRepository.updateExecutionContextPROPAGATION_REQUIREDnew--
SimpleJobRepository.updatePROPAGATION_REQUIREDnew--
SimpleJobRepository.updateExecutionContextPROPAGATION_REQUIREDnew--
SimpleJobRepository.updatePROPAGATION_REQUIREDnew--

代码列中的“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