JPA Hibernate流式查MySQL定时任务数小时后变慢,报事务回滚异常
代码存在的核心问题
流式结果集的释放逻辑存在大量泄漏路径,是故障的根本原因
- 现在的设计把查询返回的
Stream<UserServiceReminder>存在全局静态变量里,在调度类的各个异常分支零散写关闭逻辑,漏了绝大多数需要关流的场景:- 当
executeReminder()方法内抛出非TransactionSystemException的异常时,会被调度类的通用异常捕获分支接住,这个分支根本没写关流代码。流没关掉的情况下,对应的MySQL连接会一直挂着打开的流式ResultSet,既回不到Hikari连接池,也没法执行事务回滚,直接触发“Streaming ResultSet仍处于活跃状态,无法执行事务回滚”的报错。 - 关流的逻辑绑定
isReminderExecuted的返回值,一旦executeReminder()执行过程中提前抛异常,方法根本没走到return语句,返回值还是默认的false,finally块里的关流逻辑完全不会执行。 - 手动调用
stream.close()的时候没有捕获关流本身抛出的异常,一旦流已经被提前关掉、或者关流过程出IO错误,会在finally块抛新的异常,盖掉原始的报错。
- 当
- 这个问题长期累计的后果是:Hikari连接池最大才10个连接,每次任务泄漏1个连接,连续跑3小时池里的连接就会被占满,后续任务根本拿不到可用连接,只能重启进程清空状态才能恢复。
- 现在的设计把查询返回的
防重入逻辑根本挡不住并发任务执行
- 先判断Map里有没有任务标识、再把标识写进去的写法是典型的非原子check-then-act,多线程并发触发调度的时候,多个线程会同时判断到“任务没在跑”的状态,同时进执行流程,进一步抢连接资源。
- 这个全局Map存的状态没有超时兜底,一旦某次任务异常退出没清掉标识,后续所有任务都会被误判为“还在跑”直接跳过。
全局变量存流的设计本身就有问题
- 把流对象挂在全局常量类里,没有明确的归属线程:并发触发时,后启动的线程会直接把前一个线程的流引用覆盖掉,前一个线程持有的流彻底失去引用,GC回收的时候连接已经因为没正常关流被永久挂起。
- MySQL的流式查询本质是绑定长连接的,流的所有权必须和当前事务的生命周期绑定,不能跨上下文暴露给全局对象。
没有用流式查询的标准安全写法
- 用
fetchSize=Integer.MIN_VALUE开启MySQL流式读取时,只要ResultSet没读完,当前连接就不能执行任何其他SQL,也不能做事务提交/回滚。正确的写法是拿到流的第一时间就用try-with-resources包裹,从语法层面保证不管出啥异常,流都会正常关掉、连接能正常回池。 - 现在的代码把整个流遍历过程都放在事务方法里,遍历的时候往业务线程池提交任务,一旦线程池队列满了触发阻塞等待,会直接把事务持有的时间拉长,平白多占连接资源。
- 用
数据源配置和当前场景不匹配
- Hikari的泄漏检测阈值设了5分钟,但因为没关的流仍然被当前业务线程拿着,Hikari识别不出来这是泄漏连接,不会主动告警或者强制回收。
- 开了
test-on-borrow的配置,每次拿连接都要执行SELECT 1校验,但被未关闭的流式结果集占着的连接,执行校验本身就会触发驱动报错,让故障影响范围进一步扩大。
内容的提问来源于stack exchange,提问作者Yusuf Fachroni
相关产品推荐
相关产品推荐

