网络恢复后无法打开JPA EntityManager进行事务的问题解决
问题解决:网络恢复后JPA Repository持续抛出连接关闭异常
问题背景
存在一个包含5000次迭代的长循环,每次循环都会调用以下代码执行数据库查询:
if(!runnedPedidosToBulkOrCronjobClientesService.checkIsRunned(pedido.getId())){ return; }
其中checkIsRunned方法实现如下:
public Boolean checkIsRunned(Integer id_pedido){ return runnedPedidosToBulkOrCronjobClientesRepository.findById(id_pedido).isEmpty(); }
所用Repository为依赖注入的JPA Repository实例:
@Repository public interface RunnedPedidosToBulkOrCronjobClientesRepository extends JpaRepository<RunnedPedidosToBulkOrCronjobClientesEntidade,Integer> { }
该循环由HTTP请求触发,正常执行无异常,但网络中断恢复后,每次循环执行到数据库查询步骤都会抛出:
Could not open JPA EntityManager for transaction
异常详情为:
java.sql.SQLException: Connection is closed
必须重启循环才能恢复正常,现需在保留依赖注入的前提下,实现无需重启循环即可处理网络中断的方案。当前Hikari连接池配置如下:
spring.datasource.url=scrt spring.datasource.username=scrt spring.datasource.password=scrt spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver api.security.token.secret=scrt api.paginacao-size=50 server.port=9999 app.varejo-token=scrt spring.datasource.hikari.validation-timeout=5000 spring.datasource.hikari.connection-test-query=SELECT 1 spring.datasource.hikari.connection-timeout=30000 spring.datasource.hikari.maximum-pool-size=10 spring.datasource.hikari.idle-timeout=600000 spring.datasource.hikari.max-lifetime=1800000
根本原因
- 网络中断时,Hikari连接池内部分连接已失效,但因循环持续时间过长,失效连接未被及时检测和剔除,后续请求获取到了无效连接。
- 长循环若复用同一EntityManager实例,该实例持有的连接在网络中断后关闭,网络恢复后无法自动重建连接,导致后续查询持续失败。
解决方案
1. 优化Hikari连接池配置,自动剔除失效连接
在现有配置基础上补充以下参数,增强连接有效性检测:
# 获取连接前强制验证连接状态 spring.datasource.hikari.test-on-borrow=true # 空闲连接定期验证 spring.datasource.hikari.test-while-idle=true # 缩短空闲连接检测间隔,加快失效连接清理(默认60秒) spring.datasource.hikari.time-between-eviction-runs-millis=30000
这些配置会让Hikari在获取连接前、连接空闲时自动验证状态,及时剔除失效连接,确保后续请求拿到可用连接。
2. 强制每次查询使用独立事务上下文
若长循环所在方法标记了@Transactional,会导致整个循环复用同一EntityManager和连接。可将checkIsRunned方法单独配置事务传播属性,强制每次调用创建新事务和EntityManager:
@Transactional(propagation = Propagation.REQUIRES_NEW) public Boolean checkIsRunned(Integer id_pedido){ return runnedPedidosToBulkOrCronjobClientesRepository.findById(id_pedido).isEmpty(); }
每次调用该方法都会从连接池获取新连接,避免复用已失效的连接实例。
3. 添加异常捕获与重试机制
在循环中捕获数据库连接异常,设置重试逻辑,避免单次异常导致整个循环终止:
for (Pedido pedido : pedidosList) { boolean isRunned = false; int retryCount = 3; // 自定义重试次数 while (retryCount > 0) { try { isRunned = runnedPedidosToBulkOrCronjobClientesService.checkIsRunned(pedido.getId()); break; } catch (SQLException | TransactionSystemException e) { retryCount--; if (retryCount == 0) { log.error("查询订单{}状态失败,已重试3次", pedido.getId(), e); throw e; // 或根据业务逻辑跳过该订单 } // 重试前短暂等待,给连接池恢复时间 Thread.sleep(1000); } } if (!isRunned) { return; } // 后续业务逻辑 }
重试机制可在网络恢复后重新尝试获取可用连接,无需重启循环。
4. 避免长循环占用连接过长
若循环持续时间极长,可拆分循环为批量处理,或在迭代中手动清理EntityManager以释放连接:
@Autowired private EntityManager entityManager; // 每N次迭代后清理EntityManager,释放连接回池 if (iteration % 100 == 0) { entityManager.clear(); }
这会让连接及时回到连接池,后续操作重新获取可用连接。
验证步骤
- 优先调整Hikari连接池配置,添加
test-on-borrow=true和time-between-eviction-runs-millis=30000,测试网络恢复后是否自动恢复。 - 若问题仍存在,为
checkIsRunned方法添加REQUIRES_NEW事务传播属性。 - 最后补充重试机制作为兜底方案。
内容的提问来源于stack exchange,提问作者Davi Américo
相关产品推荐
相关产品推荐

