摘要

重写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的规范或最佳实践,通过标准化来避免类似问题。

参考

SpringBatch中的事务与Hibernate Session