摘要
重写Spring Batch的writer有几种方式。在THREAD中,主要有两种:
- 实现
org.springframework.batch.item.ItemWriter
接口,并重写write(List items)
方法。 - 继承
org.springframework.batch.item.database.HibernateItemWriter
类,重写write(List items)
方法。
这两种方法,都必须显式地(或通过Dao封装后)调用sessionFactory.getCurrentSession().update(item)
或merge(item)
或其它更新方法。否则,spring batch不会自动将hibernate的session中的数据同步到数据库中。
问题
THREAD系统中,目前有两个定时任务会每天更新lend_portfolio_repay_records(还款记录)表的overduedays字段。一个是RecoverCaseJob,它排除了房贷进件;另一个是UpdateOverDueDaysJob,它没有排除房贷进件。根据代码逻辑分析,问题出现在UpdateOverDueDaysJob上。
分析
直接原因
UpdateOverDueDaysJob的任务配置如下所示:
<batch:job id="updateOverDueDaysJob">
<batch:split id="updateOverDueSpilt"task-executor="asyncTaskExecutor">
<batch:flow>
<batch:step id="updateOverDueDaysStepA1"parent="abstractStep">
<batch:tasklet allow-start-if-complete="false">
<batch:chunk reader="updateOverDueDaysReaderA1"writer="updateOverDueDaysWriter"commit-interval="10"retry-limit="2">
<batch:retryable-exception-classes>
<batch:include class="org.hibernate.exception.LockTimeoutException"/>
</batch:retryable-exception-classes>
</batch:chunk>
</batch:tasklet>
</batch:step>
</batch:flow>
</batch:split>
</batch:job>
其中Reader的配置如下。检查其中的SQL,可以确定该定时任务能够扫描到房贷进件的还款记录数据。
<bean id="updateOverDueDaysReaderA1"parent="abstractCursorReader"scope="step">
<property name="queryString">
<value>from LendPortfolioRepayRecord lprr where lprr.id%4=0 and lprr.plan.lendRequest.lendChannel.id!= :channelId and lprr.plan.state = :planState</value>
</property>
<property name="fetchSize"value="10"/>
<property name="parameterValues">
<map>
<entry key="channelId"value-ref="channelId"/>
<entry key="planState"value-ref="planState"/>
</map>
</property>
</bean>
这个Job没有配置Proceesor,其Writer的代码如下。可以清楚地看到,代码中只做了r.setOverDueDays(overDueDays)
操作,并没有任何的更新数据库操作。因此,这里并不会把r实体内的数据更新到数据库中。这就是问题的直接原因。
深层原因
但是我们可以深究一点其它原因。
首先,可以推测出,当时的开发人员之所以会写这样的代码,是因为THREAD系统中开启了hibernate的自动提交,使得开发人员养成了这种只写set、不写update的代码习惯。这种配置和代码习惯固然节省了一些工作量,但是它也带来了很多问题:这里的漏写update导致未更新数据库问题;SpringBatch中的事务与Hibernate Session#问题2.1中提到的事务泄漏问题;等等。
其次,带着这种代码习惯来编写新的代码时,开发人员恐怕并没有深究spring batch中的事务与数据库操作相关代码、逻辑。如果有过研究,相信他能够在org.springframework.batch.item.database.HibernateItemWriter
类中找到currentSession.saveOrUpdate(item)
、sessionFactory.getCurrentSession().flush()
等相关代码,并能够意识到自定义Writer时,也需要做类似的处理。
最后,如果当时有做过自测,这个问题肯定能够在三年前(这个类第一次提交发生在2014年12月1日)就暴露出来。然而当时的开发人员恐怕过于自负了。
@Service
public class UpdateOverDueDaysWriter implements ItemWriter {
/** 更新逾期天数*/
@Override public void write(List<? extends LendPortfolioRepayRecord> items) {
for(LendPortfolioRepayRecord r : items) {
LendPortfolioPlan plan = r.getPlan();
// 计算还款记录的逾期天数,略;
r.setOverDueDays(overDueDays);
}
}
}
方案
方案比较简单。既可以在writer中显式的做一次数据库更新操作,也可以把set操作放到processor中、将writer设定为默认的HibernateItemWriter。目前线上的方案是将set操作放到processor中。
再一次呼唤,整理一份Spring batch的规范或最佳实践,通过标准化来避免类似问题。