[特殊字符] 事故报告:Hikari 连接池泄漏事故 - Connection leak detected

1. 问题现象

💡 简单说:系统发现有个数据库连接借出去5秒多了还没还!

复制代码
2026-01-23 10:42:39.759 [HikariPool-1 housekeeper] WARN  com.zaxxer.hikari.pool.ProxyLeakTask - Connection leak detection triggered for com.mysql.cj.jdbc.ConnectionImpl@5d665c4c on thread global-pool-2, stack trace follows
java.lang.Exception: Apparent connection leak detected
    at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:128)
    at org.springframework.jdbc.datasource.DataSourceUtils.fetchConnection(DataSourceUtils.java:158)
    at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:116)
    at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:79)
    at org.mybatis.spring.transaction.SpringManagedTransaction.openConnection(SpringManagedTransaction.java:80)
    at org.mybatis.spring.transaction.SpringManagedTransaction.getConnection(SpringManagedTransaction.java:67)

**2.**问题怎么发生的?

场景:批量处理100万条用户数据

java 复制代码
// 原本的代码逻辑
public void test() {
    List<SysUser> 一百万条数据 = 获取数据();
    // 打算分2000批处理,每批500条
        processInBatch(一百万条数据, 500, mapperClazz, (mapper, partition) -> {
            processor.accept(partition);
        });
}

问题代码核心

java 复制代码
// 有问题的批量处理方法
public <T, M> void processInBatch(List<T> list, int batchSize, Class<M> mapperClass, BiConsumer<M, List<T>> processor) {
    // 分片成2000个小任务
    // 交给线程池并发执行
    
    for (分片 : 所有分片) {
        线程池执行(() -> {
            processor.accept(分片);  // ⚠️ 问题就在这里!
        });
    }
}

🎯 根本原因:连接没还!

正常情况(单线程):借了会还

java 复制代码
员工借工具流程:
1. 去仓库借工具(获取数据库连接) ✅
2. 用工具干活(执行SQL)         ✅
3. 一小时后还工具(关闭连接)       ✅
4. 工具回到仓库(连接池)         ✅

完美闭环:借 → 用 → 还

出问题的情况(多线程):借了不还

java 复制代码
线程池员工A的悲惨一天:
早上9点:借了扳手(连接)干活任务1 → 干完没还 ❌
上午10点:直接用手里的扳手干任务2 → 干完还没还 ❌
上午11点:继续用这个扳手干任务3...
下午2点:仓库管理员发现:"扳手借出5小时了!肯定是丢了!" ⚠️

问题流程:借 → 用 → 用 → 用 → 管理员报警

🛠️ 解决方案

方案一:调整报警时间(治标不治本)

java 复制代码
# 就像把仓库的报警器调松一点
hikari:
  leakDetectionThreshold: 10000  # 10秒才报警
# 或者直接关掉报警
# leakDetectionThreshold: 0

方案二:重构代码(彻底解决)✅

核心思想:每个任务自己借,自己还!

java 复制代码
    public <T, M> void processInBatch(List<T> list, int batchSize, Class<M> mapperClass, BiConsumer<M, List<T>> processor) {
        List<List<T>> partitions = ListUtil.partition(list, batchSize);
        for (List<T> partition: partitions) {
            ThreadPoolUtil.execute(() -> {
                SqlSession session = null;
                try {
                    // 1. 自己借工具
                    session = sqlSessionFactory.openSession();

                    // 2.获取Mapper
                    M mapper = session.getMapper(mapperClass);

                    // 3. 用工具干活
                    processor.accept(mapper, partition);

                    // 4. 提交
                    session.commit();

                } catch (Exception e) {
                    // 出问题就撤回
                    session.rollback();

                } finally {
                    // 5. 必须还工具!(重点⭐)
                    session.close();  // 确保归还连接
                }
            });
        }
    }

🎯总结:

根本原因 :线程池破坏了 SqlSession 的 ThreadLocal 生命周期管理机制。

正常情况 (单线程):

Spring/MyBatis 通过 ThreadLocal 管理 SqlSession,当线程结束时,框架会在 finally 中自动调用 close() 归还连接。

问题场景 (线程池):

线程池中的线程是长期存活且复用的,不会"自然结束"。这导致:

  • SqlSession 通过 ThreadLocal 绑定到线程池线程后无法自动清理

  • 连接被长期持有且永不归还

  • 框架依赖的"线程结束即清理"机制完全失效

本质 :

线程池的长生命周期特性 与 ThreadLocal 的短生命周期假设发生了根本冲突,导致数据库连接"有借无还"。

相关推荐
wuminyu3 小时前
JVM规范学习总结:解释器与JIT编译器如何处理JVM字节码
java·linux·c语言·jvm·c++
txxzjmzlh3 小时前
SpringBoot 配置⽂件
java·spring boot·spring
小徐不徐说4 小时前
序列化与反序列化:把 C++ 对象可靠地保存下来
开发语言·数据库·c++·php
ragnwang4 小时前
Notes: Python pybind11 import C++ module
开发语言·c++·python
Wang's Blog4 小时前
Java 项目实战: 交友项目 - 搜索附近的功能概述和分析
java·开发语言·交友
骑着蜗牛撵大象3274 小时前
大模型 Agent 的沙箱护栏:gVisor 隔离、出口白名单与审计闭环
java·开发语言·jvm·安全·agent沙箱·gvisor·microvm
独泪了无痕4 小时前
Hutool之LocalDateTimeUtil:让时间间隔操作如虎添翼
java
Dxy12393102165 小时前
JS DES 加密详解
开发语言·javascript·arcgis
充值内卷5 小时前
我给桌面加了个鱼缸:C++ 写后端,WebView2 里养一座 Three.js 小岛
开发语言·javascript·c++
云和数据.ChenGuang5 小时前
深入解析 Python 的 dir():从用法到 CPython 源码实现
开发语言·人工智能·python·langchain