You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Batch JdbcPagingItemReader读取遗漏最后一条数据问题

问题排查:Spring Batch JdbcPagingItemReader 遗漏最后一条记录

表结构与主键

表数据示例:

CNMA_CO_PLATFORM_MESSAGE|AUDI_TI_CREATION|FIELD4|OTHER FIELDS
test-jj@2774@20210422112434957@00026129|22/04/21 11:24:34,957000000|11|..
test-jj2@2774@20210422112434957@00026129|22/04/21 11:24:34,957000000|12|..
test-jj3@2774@20210422112434957@00026129|22/04/21 11:24:34,957000000|13|..

组合主键为:CNMA_CO_PLATFORM_MESSAGE|AUDI_TI_CREATION

JdbcPagingItemReader 配置

@StepScope
@Bean
public JdbcPagingItemReader<PendingNotificationDTO> pendingNotificationReader(
        @Value("#{stepExecution}") StepExecution stepExecution){
    
    final JdbcPagingItemReader<PendingNotificationDTO> reader = new JdbcPagingItemReader<>();
    reader.setDataSource(daoDataSource);
    reader.setName("pendingNotificationReader");
    
    //构建查询语句
    final OraclePagingQueryProvider oraclePagingQueryProvider = new OraclePagingQueryProvider();
    oraclePagingQueryProvider.setSelectClause("SELECT   " + 
            "    cegct.AUDI_TI_CREATION,                "+"    CNMA_CO_PLATFORM_MESSAGE,              " + 
            "    OTHERFIELDS...                     ");
    oraclePagingQueryProvider.setFromClause("FROM TABLE1 cegct " + 
            "   JOIN TABLE1 notip ON cegct.field1 = notip.field1 " +
            "   AND notip.field2 = :frSur ");
    oraclePagingQueryProvider.setWhereClause("WHERE "
            + " cegct.field3 = 0 "
            + " AND cegct.field4 in (:notifStatusList) ");
    
    //指定用于分页的唯一字段集合
    Map<String, Order> sortKeys = new HashMap<>();
    sortKeys.put("CNMA_CO_PLATFORM_MESSAGE", Order.DESCENDING);
    sortKeys.put("AUDI_TI_CREATION", Order.DESCENDING);

    oraclePagingQueryProvider.setSortKeys(sortKeys );
    
    reader.setQueryProvider(oraclePagingQueryProvider);
    
    String frSur        = stepExecution.getJobExecution().getExecutionContext().getString(Constants.FM_ROLE_SUR_ZK);
    String notifStatus  = stepExecution.getJobExecution().getExecutionContext().getString(Constants.STATUS_REPORTS);
    
    Map<String, Object> parameters = new HashMap<>();
    parameters.put("frSur", frSur);
    parameters.put("notifStatusList", Arrays.asList(StringUtils.split(notifStatus, ",")));
    reader.setParameterValues(parameters );
    
    Integer initLoaded  = stepExecution.getJobExecution().getExecutionContext().getInt(Constants.RECOVER_PENDING_NOT_COMMIT);
    reader.setPageSize(initLoaded);
    reader.setRowMapper(new BeanPropertyRowMapper<PendingNotificationDTO>(PendingNotificationDTO.class));
    
    return reader; 
    
}

测试现象

3条数据均符合查询条件,分页大小设为1时:

  • 第一个chunk读取到test-jj3@...记录
  • 第二个chunk读取到test-jj2@...记录
  • 第三个chunk未读取到test-jj@...记录(预期应读取该条)

生成的SQL语句

第一个chunk(查询1条记录)

SELECT * FROM (
    SELECT 
        cegct.AUDI_TI_CREATION                      
        CNMA_CO_PLATFORM_MESSAGE, [otherfields] 
    
    FROM   [FROM] 
    WHERE  [where]
    ORDER BY CNMA_CO_PLATFORM_MESSAGE DESC, AUDI_TI_CREATION DESC

) WHERE ROWNUM <= 1;

第二个chunk(查询1条记录)

SELECT * FROM (
    SELECT 
        cegct.AUDI_TI_CREATION                      
        CNMA_CO_PLATFORM_MESSAGE, [otherfields] 
    
    FROM   [FROM] 
    WHERE  [where]
    ORDER BY CNMA_CO_PLATFORM_MESSAGE DESC, AUDI_TI_CREATION DESC
) WHERE 
ROWNUM <= 1 AND (
    (CNMA_CO_PLATFORM_MESSAGE < 'test-jj3@2774@20210422112434957@00026129') 
    OR 
    (CNMA_CO_PLATFORM_MESSAGE = 'test-jj3@2774@20210422112434957@00026129' AND AUDI_TI_CREATION < TO_DATE('2021-04-22 11:24:34', 'YYYY-MM-DD HH24:MI:SS'))
);

第三个chunk(查询0条记录)

SELECT * FROM (
    SELECT 
        cegct.AUDI_TI_CREATION                      
        CNMA_CO_PLATFORM_MESSAGE, [otherfields] 
    
    FROM   [FROM] 
    WHERE  [where]
    ORDER BY CNMA_CO_PLATFORM_MESSAGE DESC, AUDI_TI_CREATION DESC
) WHERE 
ROWNUM <= 1 AND (
    (CNMA_CO_PLATFORM_MESSAGE < 'test-jj2@2774@20210422112434957@00026129') 
    OR 
    (CNMA_CO_PLATFORM_MESSAGE = 'test-jj2@2774@20210422112434957@00026129' AND AUDI_TI_CREATION < TO_DATE('2021-04-22 11:24:34', 'YYYY-MM-DD HH24:MI:SS'))
);

问题原因分析

  1. 字符串排序与业务预期不符:
    数据库字符串比较按ASCII值逐位进行,test-jj@...与test-jj2@...前7个字符完全相同,第8位@(ASCII 64)数值大于2(ASCII 50),因此test-jj@...的字符串值大于test-jj2@...。第三个chunk的过滤条件是查找CNMA_CO_PLATFORM_MESSAGE < 'test-jj2@...'的记录,test-jj@...不满足该条件,因此被排除。

  2. 时间比较丢失毫秒精度:
    生成的SQL使用TO_DATE('2021-04-22 11:24:34', ...)截断了毫秒部分,但实际字段值包含毫秒(22/04/21 11:24:34,957000000),导致相同秒数的时间比较条件永远不满足。该问题当前未影响结果,但会导致相同CNMA_CO_PLATFORM_MESSAGE下的多记录分页异常。

解决方案

方案1:调整字段命名格式

将test-jj@...改为test-jj0@...,让数字部分补零,确保字符串排序顺序与业务预期一致(test-jj3@... > test-jj2@... > test-jj0@...)。

方案2:基于数字部分排序

修改排序键,提取CNMA_CO_PLATFORM_MESSAGE中的数字部分进行排序,避免字符串比较的歧义:

Map<String, Order> sortKeys = new HashMap<>();
// 提取test-jj后的数字部分转为数值排序
sortKeys.put("TO_NUMBER(REGEXP_SUBSTR(CNMA_CO_PLATFORM_MESSAGE, 'test-jj(\\d*)@', 1, 1, NULL, 1))", Order.DESCENDING);
sortKeys.put("AUDI_TI_CREATION", Order.DESCENDING);
oraclePagingQueryProvider.setSortKeys(sortKeys);

注意:需确保所有CNMA_CO_PLATFORM_MESSAGE符合格式要求,建议创建对应函数索引优化性能。

方案3:修复时间比较精度

自定义OraclePagingQueryProvider的日期处理逻辑,使用TO_TIMESTAMP替代TO_DATE,保留毫秒精度,确保时间比较准确。

内容的提问来源于stack exchange,提问作者JuniorGuy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 20:25:47