Spring Batch 8分区Chunk任务无报错卡在STARTED状态排查
根因拆解
- Spring Batch 3.0.6 原生分区执行的异常吞没问题
该版本内置的TaskExecutorPartitionHandler基于池化线程调度分区任务,当分区线程抛出未捕获的RuntimeException/Error时,异常会被线程池无界队列的执行逻辑静默拦截,不会将“分区执行中断”的信号回传给主作业状态机。主作业会一直挂起等待所有分区的完成回执,永远不会触发作业状态更新,完全符合“无报错日志、作业/步骤始终停留在STARTED”的现象。这类场景在分区线程遇到OOM被系统强制终止、线程栈被外部中断时尤为明显——异常根本不会走到日志输出链路,更不会触发状态回传。 - JdbcCursorItemReader 与Postgres长连接的静默阻塞问题
JdbcCursorItemReader默认基于Postgres服务器端游标读取数据,整个读取流程需要长期持有单条数据库连接,两次fetch数据的间隔可能长达数分钟(百万级数据处理的场景下十分常见)。叠加Postgres9.2、上层连接池、中间网络设备的多层空闲连接回收策略,会出现无感知的连接断开:数据库侧因空闲事务超时、连接数上限清理会话时,会直接清除pg_stat_activity中的对应记录;此时如果JDBC连接未配置socketTimeout参数,客户端分区线程会永远阻塞在TCP读系统调用上,既无法抛出连接断开的异常,也无法继续执行后续处理逻辑,最终表现为分区线程“消失”、作业状态完全卡死。这类TCP半开连接的场景下,应用层完全感知不到连接已经失效,自然不会输出错误日志。 - 连接池的长连接误回收问题
长耗时的游标读取场景中,连接池的空闲连接判定逻辑很容易把“持有连接但两次fetch间隔较长”的游标连接判定为空闲连接,直接回收物理连接。但JdbcCursorItemReader内部还持有该连接的游标引用,后续尝试fetch数据时,老版本Postgres驱动的ResultSet实现会将连接重置异常静默处理(直接返回空结果),导致reader进入无限等待下一条数据的状态,分区线程永远不会正常退出。
可落地解决思路
- 修复3.0.6版本的分区执行逻辑缺陷
- 优先升级到3.0.x分支的最新补丁版本(3.0.10),该版本已修复
TaskExecutorPartitionHandler未捕获分区线程异常、未同步分区执行状态给主作业的核心问题。如果无法完成版本升级,可以自定义实现PartitionHandler,在每个分区的任务执行逻辑外层加全量try-catch,无论是否捕获到异常,都显式将分区的StepExecution状态标记为完成/失败,通过JobRepository回传主作业,避免主作业无限等待。 - 给分区线程池配置全局
Thread.UncaughtExceptionHandler,将所有未捕获的异常直接输出到日志,同时触发主作业的状态更新,从机制上杜绝异常被静默吞掉的问题。
- 优先升级到3.0.x分支的最新补丁版本(3.0.10),该版本已修复
- 适配JdbcCursorItemReader与Postgres的连接交互逻辑
- 给Postgres JDBC连接显式配置
socketTimeout参数(推荐设置为30分钟),让客户端在连接无响应时主动抛出超时异常,避免永远阻塞在TCP读操作上。 - 调整游标读取参数:将
setFetchSize设置为1000-5000的合理区间,缩短两次fetch的间隔;同步调整连接池配置,仅对“持有连接超过指定时长且无任何读写操作”的连接判定为空闲,避免误回收正在执行游标读取的长连接。 - 针对Postgres9.2的兼容特性,关闭长事务依赖的服务器端游标,切换为客户端侧流式读取模式:设置
setVerifyCursorPosition(false)、将连接autoCommit属性设置为true,让驱动在本地缓存读取结果,避免长时间持有事务导致连接被服务端强制回收。
- 给Postgres JDBC连接显式配置
- 补全作业执行的状态监控兜底
- 给每个分区步骤增加心跳上报机制:每隔1分钟在StepExecution的ExecutionContext中写入当前处理行数、上报时间戳;主作业等待分区完成时,若超过设定阈值(比如2小时)未收到某分区的心跳,主动标记该分区执行超时,终止整个作业流程,避免无限卡在STARTED状态。
- 出现作业卡死时,第一时间导出JVM线程栈,直接定位分区线程是阻塞在数据库读操作上、还是已异常退出,快速缩小排查范围。
内容的提问来源于stack exchange,提问作者DevJava
相关产品推荐
相关产品推荐

