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')) );
问题原因分析
字符串排序与业务预期不符:
数据库字符串比较按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@...不满足该条件,因此被排除。时间比较丢失毫秒精度:
生成的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

