1. 从一次诡异的“连接已关闭”异常说起那天下午系统监控突然开始疯狂报警。我点开错误日志满屏都是java.sql.SQLException: connection closed而且都集中在事务回滚的时候。具体报错信息和很多朋友遇到的一样是 MyBatis 在尝试rollback时发现手里的数据库连接Connection已经被关闭了导致事务回滚失败进而引发一系列数据不一致的连锁反应。这感觉就像你正拿着钥匙准备锁门结果门自己“砰”一声关上了还从里面反锁了把你晾在门外。更让人头疼的是这个问题不是必现的它像幽灵一样在业务高峰期偶尔出现线下测试环境又很难复现排查起来非常棘手。我第一反应是去检查代码是不是有地方手动调用了connection.close()而没有正确归还给连接池或者是不是在复杂的异步、多线程场景下连接被错误地共享和关闭了一通排查下来代码逻辑看起来并没有明显问题。这时候经验告诉我很可能是遇到了数据库连接泄露。什么是连接泄露简单来说就是从连接池比如 Druid借出去的连接用完之后没有还回来。想象一下图书馆借书你借了书连接回家业务线程看完执行完SQL后却忘了还或者因为某些意外比如你突然被叫走没来得及还。如果每个人都这样图书馆连接池的书很快就会被借空后面的人新的业务请求就无书可借导致系统卡死或报错。Druid 作为一款强大的数据库连接池早就为我们想到了这一点它内置了一套“泄露检测与回收”机制核心就是removeAbandoned相关参数。这次遇到的“Connection Closed”异常正是开启这套机制后Druid 主动回收“逾期未还”的连接时可能触发的副作用之一。下面我就带你彻底搞懂这套机制从原理到配置手把手解决这个烦人的问题。2. 深入原理Druid 如何发现并回收“流浪”的连接要解决问题得先明白问题是怎么产生的。Druid 的连接泄露检测机制官方称之为“RemoveAbandoned”它的工作原理就像一个尽职的图书管理员。2.1 核心机制与“图书管理员”类比在 Druid 连接池中每当你通过DataSource.getConnection()借出一个连接时Druid 会记录下这个连接的“借出时间”和“借阅者”当前线程的堆栈信息。在后台有一个名为DestroyTask的定时任务就是那位“图书管理员”它会按照固定的频率由timeBetweenEvictionRunsMillis参数控制默认1分钟来巡检所有已借出但尚未归还的连接。它的检查逻辑非常直接检查连接是否仍处于“借出”状态。检查这个连接从借出到现在是否已经超过了设定的“最大借阅时长”即removeAbandonedTimeoutMillis默认5分钟。检查这个连接在“最大借阅时长”内是否执行过任何SQL通过检查连接内部的一个执行状态标记。如果同时满足以上三个条件——连接被借出、超过5分钟、且在这5分钟内“一动不动”没执行过SQL——那么 Druid 就会判定这个连接“泄露”了。它不再等待原借阅者归还而是会采取强制措施。2.2 “强制回收”与异常的产生Druid 的强制回收分为几个步骤而问题就出在这里打印泄露日志如果logAbandonedtrue管理员会记下是哪本书连接被谁线程堆栈借走未还方便我们溯源。回滚该连接上的任何活跃事务这是关键一步。为了保证数据一致性Druid 会在回收前尝试执行connection.rollback()。真正关闭物理连接调用connection.close()将这个“流浪”的连接彻底关闭释放数据库服务端的资源。从连接池活跃列表中移除将这个连接标记为已回收。那么java.sql.SQLException: connection closed这个异常是在哪一步抛出的呢它发生在原业务线程试图操作这个已被回收的连接时。我们还原一下案发现场T1时刻业务线程A从Druid借出连接Conn1开始一个事务执行了一些更新操作。T2时刻例如2分钟后由于代码bug、复杂逻辑分支或异常处理不当线程A没有正确调用close()方法归还Conn1。Conn1处于“借出但闲置”状态。T3时刻又过了3分钟总时长5分钟Druid 的DestroyTask巡检线程启动发现Conn1已借出5分钟且期间无活动判定其泄露。T3.1时刻Druid 强制对Conn1执行rollback()和close()。此时数据库服务端认为Conn1已经关闭。T4时刻业务线程A的代码继续执行终于走到了事务回滚或提交的逻辑例如sqlSession.rollback()。MyBatis 在这个方法里会去获取 Conn1 的autoCommit状态等但此时 Conn1 在客户端驱动层面可能还未被标记为无效直到真正发起网络调用时驱动才发现“此路不通”连接已关闭于是抛出了SQLException: connection closed。所以这个异常不是Druid回收机制本身的错误而是机制生效后暴露了业务代码中存在的连接泄露缺陷。它像一个警报告诉我们“嗨你的代码里有连接没还我帮你强制清理了但原来的操作会失败哦。”3. 实战配置一步步调教你的泄露检测机制理解了原理配置起来就心中有数了。我们以 Spring Boot 集成 Druid 为例详细讲解每个参数。这里提供一个完整、可落地的配置模板并解释关键项。3.1 基础泄露检测配置这是解决当前问题的核心配置通常放在application.yml中。spring: datasource: dynamic: # 如果你用了动态数据源配置在此下如果是单数据源则是 spring.datasource.druid druid: # 1. 开启连接泄露检测与回收总开关 remove-abandoned: true # 2. 连接被判定为泄露的超时时间单位毫秒 remove-abandoned-timeout-millis: 300000 # 默认300000即5分钟 # 3. 是否打印泄露连接的堆栈信息强烈建议开启 log-abandoned: true参数详解remove-abandoned:总开关。必须设置为true泄露检测机制才会生效。生产环境建议长期开启作为一道安全防线。remove-abandoned-timeout-millis:核心阈值。这个值需要根据你的业务场景谨慎设置。值太小如1分钟可能导致误杀。一些执行时间较长的批处理任务、复杂查询或等待外部响应的业务可能正常执行时间就超过1分钟但连接并非泄露。这会造成不必要的连接中断和业务报错。值太大如30分钟失去检测意义。真正的泄露连接要等待很久才会被回收在此期间会持续占用连接池资源可能导致池子耗尽。5分钟300000毫秒是一个比较通用的起始值。你可以观察线上业务的SQL执行时间分布将其设置为(P99耗时 一定的缓冲时间)。例如你系统99%的SQL在2秒内完成那么设置2-5分钟是比较安全的。log-abandoned:诊断利器。强烈建议设为true。当Druid回收一个连接时会在日志通常是WARN级别中打印出该连接被借出时的调用堆栈。这个堆栈信息是定位泄露代码位置的黄金线索它能直接告诉你是哪一行代码借走了连接却没有归还。3.2 与连接池其他关键参数的协同泄露检测机制不是孤立的它需要和连接池的其他健康检查参数配合工作才能达到最佳效果。spring: datasource: dynamic: druid: # ... 上述泄露检测配置 ... # 连接池大小配置 initial-size: 5 min-idle: 5 max-active: 20 # 连接有效性检查配置与泄露检测互补 test-while-idle: true # 重要在空闲时检查连接是否有效平衡性能与安全 test-on-borrow: false # 借出时检查影响性能不建议开启 test-on-return: false # 归还时检查影响性能不建议开启 validation-query: SELECT 1 # 简单的验证SQLMySQL可用Oracle需换为 SELECT 1 FROM DUAL time-between-eviction-runs-millis: 60000 # 后台清理线程的运行周期也影响泄露检查频率 min-evictable-idle-time-millis: 1800000 # 连接在池中最小空闲生存时间默认30分钟 # 监控相关可选但推荐 filters: stat,wall,slf4j # 启用统计、防火墙和日志过滤器 web-stat-filter: enabled: true stat-view-servlet: enabled: true协同工作流time-between-eviction-runs-millis默认60秒决定了“图书管理员”多久巡检一次。它同时驱动着空闲连接回收 (test-while-idle) 和泄露连接检测 (remove-abandoned)。每次巡检时管理员会检查空闲时间超过min-evictable-idle-time-millis的连接如果无效就关闭。检查借出时间超过remove-abandoned-timeout-millis且无活动的连接进行强制回收。test-while-idle保证了池子里空闲连接的健康而remove-abandoned管的是那些“借出去不还”的连接。两者目标不同但共同保障了连接池的可用性。3.3 针对特定场景的调优技巧场景一批量任务或长事务系统如果你的系统有夜间跑批任务或者某些业务事务本身就很长例如处理一个大型文件5分钟的泄露超时可能太短。解决方案为这类特定任务单独配置一个数据源并设置更长的remove-abandoned-timeout-millis例如30分钟。或者在代码层面确保这些长耗时任务使用后立即归还连接避免依赖超时回收。可以考虑使用try-with-resources语法Java 7确保连接关闭。场景二高并发、短平快业务对于电商秒杀、API网关等场景业务响应要求在毫秒级连接借出时间极短。解决方案可以适当调低remove-abandoned-timeout-millis例如设置为1分钟60000。这样能更快地回收真正的泄露连接释放资源。同时确保max-active最大连接数设置合理避免在泄露连接被快速回收前池子就已耗尽。场景三希望记录泄露但不立即回收用于观察在某些调试阶段你可能想看看有多少泄露发生但又不想影响当前正在执行的、可能超时的业务。变通方案保持remove-abandoned: true和log-abandoned: true。将remove-abandoned-timeout-millis设置为一个非常大的值例如1小时或更长。这样泄露连接会在日志中被记录通过log-abandoned但不会在短时间内被强制回收方便你收集足够的堆栈信息进行分析而不会立即引发回滚异常。4. 代码层面如何从根本上杜绝连接泄露配置只是治标写出健壮的代码才是治本。再好的回收机制也只是最后的保险丝。我们应该追求的是代码本身不泄露任何连接。4.1 最佳实践使用 try-with-resources这是 Java 7 以来处理必须关闭的资源如Connection,Statement,ResultSet的首选和最强力推荐的方式。它能保证在代码块执行完毕后无论是否发生异常资源都会被自动关闭。// 传统方式容易忘记关闭或在异常处理中漏关 Connection conn null; Statement stmt null; ResultSet rs null; try { conn dataSource.getConnection(); stmt conn.createStatement(); rs stmt.executeQuery(SELECT * FROM user); // ... 处理结果 } catch (SQLException e) { // 处理异常 } finally { // 需要手动、反向关闭非常繁琐且易错 if (rs ! null) try { rs.close(); } catch (SQLException ignore) {} if (stmt ! null) try { stmt.close(); } catch (SQLException ignore) {} if (conn ! null) try { conn.close(); } catch (SQLException ignore) {} } // 现代方式使用try-with-resources绝对安全 try (Connection conn dataSource.getConnection(); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(SELECT * FROM user)) { // ... 处理结果 } catch (SQLException e) { // 处理异常 } // 无需finally块连接、语句、结果集会在try块结束时自动、正确地关闭。在 Spring MyBatis 环境中我们通常不直接操作Connection而是通过SqlSession或Mapper接口。确保你获取的SqlSession在正确的范围内被关闭。在 Spring 管理的事务中这通常是自动完成的。4.2 框架使用规范MyBatis 与 Spring 事务1. 确保在 Service 层使用Transactional业务操作应该在 Service 层方法上声明事务。这样Spring 会帮你管理连接的获取、提交/回滚和归还。避免在 DAO/Mapper 层手动控制事务。Service public class UserService { Autowired private UserMapper userMapper; Transactional // Spring 会在此方法开始时获取连接方法结束时根据是否异常决定提交或回滚并归还连接。 public void updateUser(User user) { userMapper.update(user); // 其他数据库操作... } }2. 警惕“连接穿透”不要在 Service 方法中将DataSource或Connection对象传递给其他非受 Spring 事务管理的方法或线程这会导致连接生命周期管理混乱。3. 正确处理SqlSession如果你需要手动管理SqlSession例如在非 Web 环境的工具类中必须使用try-with-resources或确保在 finally 块中关闭。try (SqlSession session sqlSessionFactory.openSession()) { UserMapper mapper session.getMapper(UserMapper.class); mapper.insert(user); session.commit(); // 手动提交 } // session 会自动关闭连接归还给连接池4.3 利用logAbandoned堆栈信息定位问题当你在日志中看到 Druid 打印的泄露警告时类似下面这样WARN - com.alibaba.druid.pool.DruidDataSource - abandon connection, owner thread: http-nio-8080-exec-5, connected at: 1672531200000, open stackTrace at com.example.YourService.yourMethod(YourService.java:50) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1080) ...重点看at com.example.YourService.yourMethod(YourService.java:50)这一行及其附近的调用栈。这行代码就是最初借出连接但没有归还的地方。你需要检查这个方法是否有分支逻辑如if/else,return提前退出导致连接没有走到关闭逻辑是否在循环中获取连接但只在循环外关闭了一次是否在异步回调或子线程中使用了连接但没有正确管理其生命周期根据堆栈信息去审查对应代码十有八九能找到泄露的根源。5. 进阶源码浅析与问题深度规避对于想更深入了解的同学我们可以简单看看 Druid 相关源码这能加深理解并在遇到更复杂问题时提供思路。removeAbandoned的核心逻辑在DruidDataSource类的removeAbandoned()方法中。它会遍历activeConnections活跃连接列表对每个连接检查其lastActiveTime和connectedTime。如果(当前时间 - connectedTime) removeAbandonedTimeoutMillis并且连接处于空闲状态则将其加入待回收列表。一个重要的注意点是Druid 在回收连接时会先调用conn.rollback()。这意味着如果你的业务代码在连接泄露期间修改了数据但未提交这些修改会被 Druid 强制回滚这可能导致数据丢失。因此绝不能依赖超时回收来代替正确的事务提交。深度规避建议代码审查与静态分析将“连接泄露”作为代码审查的重点项。可以使用 SonarQube 等静态代码分析工具它们能检测出未关闭的资源Resource not closed这类问题。集成测试编写集成测试模拟长时间运行或异常退出的场景然后检查测试结束后连接池的状态可以通过 Druid 的监控界面或 JMX确保没有连接泄露。监控与告警持续监控生产环境 Druid 连接池的ActiveCount、RemoveAbandonedCount等关键指标。如果RemoveAbandonedCount持续大于0说明系统存在持续的连接泄露需要立即告警并排查。Druid 自带的stat-view-servlet和Spring Boot Actuator集成是很好的监控手段。配置好removeAbandoned机制就像是给系统加装了一个烟雾报警器。它不能防止火灾代码bug但能在火势蔓延连接池耗尽之前发出警报甚至自动启动喷淋回收连接。真正的安全来自于规范的代码编写和严谨的资源管理习惯。希望这篇从异常现象到源码原理再到实战配置和代码规范的长文能帮你彻底驯服数据库连接泄露这只“房间里的大象”。下次再看到connection closed你就能从容地把它变成优化系统稳定性的一个契机了。
【实战】Druid连接池泄露检测机制配置详解:从Connection Close异常到高效解决
1. 从一次诡异的“连接已关闭”异常说起那天下午系统监控突然开始疯狂报警。我点开错误日志满屏都是java.sql.SQLException: connection closed而且都集中在事务回滚的时候。具体报错信息和很多朋友遇到的一样是 MyBatis 在尝试rollback时发现手里的数据库连接Connection已经被关闭了导致事务回滚失败进而引发一系列数据不一致的连锁反应。这感觉就像你正拿着钥匙准备锁门结果门自己“砰”一声关上了还从里面反锁了把你晾在门外。更让人头疼的是这个问题不是必现的它像幽灵一样在业务高峰期偶尔出现线下测试环境又很难复现排查起来非常棘手。我第一反应是去检查代码是不是有地方手动调用了connection.close()而没有正确归还给连接池或者是不是在复杂的异步、多线程场景下连接被错误地共享和关闭了一通排查下来代码逻辑看起来并没有明显问题。这时候经验告诉我很可能是遇到了数据库连接泄露。什么是连接泄露简单来说就是从连接池比如 Druid借出去的连接用完之后没有还回来。想象一下图书馆借书你借了书连接回家业务线程看完执行完SQL后却忘了还或者因为某些意外比如你突然被叫走没来得及还。如果每个人都这样图书馆连接池的书很快就会被借空后面的人新的业务请求就无书可借导致系统卡死或报错。Druid 作为一款强大的数据库连接池早就为我们想到了这一点它内置了一套“泄露检测与回收”机制核心就是removeAbandoned相关参数。这次遇到的“Connection Closed”异常正是开启这套机制后Druid 主动回收“逾期未还”的连接时可能触发的副作用之一。下面我就带你彻底搞懂这套机制从原理到配置手把手解决这个烦人的问题。2. 深入原理Druid 如何发现并回收“流浪”的连接要解决问题得先明白问题是怎么产生的。Druid 的连接泄露检测机制官方称之为“RemoveAbandoned”它的工作原理就像一个尽职的图书管理员。2.1 核心机制与“图书管理员”类比在 Druid 连接池中每当你通过DataSource.getConnection()借出一个连接时Druid 会记录下这个连接的“借出时间”和“借阅者”当前线程的堆栈信息。在后台有一个名为DestroyTask的定时任务就是那位“图书管理员”它会按照固定的频率由timeBetweenEvictionRunsMillis参数控制默认1分钟来巡检所有已借出但尚未归还的连接。它的检查逻辑非常直接检查连接是否仍处于“借出”状态。检查这个连接从借出到现在是否已经超过了设定的“最大借阅时长”即removeAbandonedTimeoutMillis默认5分钟。检查这个连接在“最大借阅时长”内是否执行过任何SQL通过检查连接内部的一个执行状态标记。如果同时满足以上三个条件——连接被借出、超过5分钟、且在这5分钟内“一动不动”没执行过SQL——那么 Druid 就会判定这个连接“泄露”了。它不再等待原借阅者归还而是会采取强制措施。2.2 “强制回收”与异常的产生Druid 的强制回收分为几个步骤而问题就出在这里打印泄露日志如果logAbandonedtrue管理员会记下是哪本书连接被谁线程堆栈借走未还方便我们溯源。回滚该连接上的任何活跃事务这是关键一步。为了保证数据一致性Druid 会在回收前尝试执行connection.rollback()。真正关闭物理连接调用connection.close()将这个“流浪”的连接彻底关闭释放数据库服务端的资源。从连接池活跃列表中移除将这个连接标记为已回收。那么java.sql.SQLException: connection closed这个异常是在哪一步抛出的呢它发生在原业务线程试图操作这个已被回收的连接时。我们还原一下案发现场T1时刻业务线程A从Druid借出连接Conn1开始一个事务执行了一些更新操作。T2时刻例如2分钟后由于代码bug、复杂逻辑分支或异常处理不当线程A没有正确调用close()方法归还Conn1。Conn1处于“借出但闲置”状态。T3时刻又过了3分钟总时长5分钟Druid 的DestroyTask巡检线程启动发现Conn1已借出5分钟且期间无活动判定其泄露。T3.1时刻Druid 强制对Conn1执行rollback()和close()。此时数据库服务端认为Conn1已经关闭。T4时刻业务线程A的代码继续执行终于走到了事务回滚或提交的逻辑例如sqlSession.rollback()。MyBatis 在这个方法里会去获取 Conn1 的autoCommit状态等但此时 Conn1 在客户端驱动层面可能还未被标记为无效直到真正发起网络调用时驱动才发现“此路不通”连接已关闭于是抛出了SQLException: connection closed。所以这个异常不是Druid回收机制本身的错误而是机制生效后暴露了业务代码中存在的连接泄露缺陷。它像一个警报告诉我们“嗨你的代码里有连接没还我帮你强制清理了但原来的操作会失败哦。”3. 实战配置一步步调教你的泄露检测机制理解了原理配置起来就心中有数了。我们以 Spring Boot 集成 Druid 为例详细讲解每个参数。这里提供一个完整、可落地的配置模板并解释关键项。3.1 基础泄露检测配置这是解决当前问题的核心配置通常放在application.yml中。spring: datasource: dynamic: # 如果你用了动态数据源配置在此下如果是单数据源则是 spring.datasource.druid druid: # 1. 开启连接泄露检测与回收总开关 remove-abandoned: true # 2. 连接被判定为泄露的超时时间单位毫秒 remove-abandoned-timeout-millis: 300000 # 默认300000即5分钟 # 3. 是否打印泄露连接的堆栈信息强烈建议开启 log-abandoned: true参数详解remove-abandoned:总开关。必须设置为true泄露检测机制才会生效。生产环境建议长期开启作为一道安全防线。remove-abandoned-timeout-millis:核心阈值。这个值需要根据你的业务场景谨慎设置。值太小如1分钟可能导致误杀。一些执行时间较长的批处理任务、复杂查询或等待外部响应的业务可能正常执行时间就超过1分钟但连接并非泄露。这会造成不必要的连接中断和业务报错。值太大如30分钟失去检测意义。真正的泄露连接要等待很久才会被回收在此期间会持续占用连接池资源可能导致池子耗尽。5分钟300000毫秒是一个比较通用的起始值。你可以观察线上业务的SQL执行时间分布将其设置为(P99耗时 一定的缓冲时间)。例如你系统99%的SQL在2秒内完成那么设置2-5分钟是比较安全的。log-abandoned:诊断利器。强烈建议设为true。当Druid回收一个连接时会在日志通常是WARN级别中打印出该连接被借出时的调用堆栈。这个堆栈信息是定位泄露代码位置的黄金线索它能直接告诉你是哪一行代码借走了连接却没有归还。3.2 与连接池其他关键参数的协同泄露检测机制不是孤立的它需要和连接池的其他健康检查参数配合工作才能达到最佳效果。spring: datasource: dynamic: druid: # ... 上述泄露检测配置 ... # 连接池大小配置 initial-size: 5 min-idle: 5 max-active: 20 # 连接有效性检查配置与泄露检测互补 test-while-idle: true # 重要在空闲时检查连接是否有效平衡性能与安全 test-on-borrow: false # 借出时检查影响性能不建议开启 test-on-return: false # 归还时检查影响性能不建议开启 validation-query: SELECT 1 # 简单的验证SQLMySQL可用Oracle需换为 SELECT 1 FROM DUAL time-between-eviction-runs-millis: 60000 # 后台清理线程的运行周期也影响泄露检查频率 min-evictable-idle-time-millis: 1800000 # 连接在池中最小空闲生存时间默认30分钟 # 监控相关可选但推荐 filters: stat,wall,slf4j # 启用统计、防火墙和日志过滤器 web-stat-filter: enabled: true stat-view-servlet: enabled: true协同工作流time-between-eviction-runs-millis默认60秒决定了“图书管理员”多久巡检一次。它同时驱动着空闲连接回收 (test-while-idle) 和泄露连接检测 (remove-abandoned)。每次巡检时管理员会检查空闲时间超过min-evictable-idle-time-millis的连接如果无效就关闭。检查借出时间超过remove-abandoned-timeout-millis且无活动的连接进行强制回收。test-while-idle保证了池子里空闲连接的健康而remove-abandoned管的是那些“借出去不还”的连接。两者目标不同但共同保障了连接池的可用性。3.3 针对特定场景的调优技巧场景一批量任务或长事务系统如果你的系统有夜间跑批任务或者某些业务事务本身就很长例如处理一个大型文件5分钟的泄露超时可能太短。解决方案为这类特定任务单独配置一个数据源并设置更长的remove-abandoned-timeout-millis例如30分钟。或者在代码层面确保这些长耗时任务使用后立即归还连接避免依赖超时回收。可以考虑使用try-with-resources语法Java 7确保连接关闭。场景二高并发、短平快业务对于电商秒杀、API网关等场景业务响应要求在毫秒级连接借出时间极短。解决方案可以适当调低remove-abandoned-timeout-millis例如设置为1分钟60000。这样能更快地回收真正的泄露连接释放资源。同时确保max-active最大连接数设置合理避免在泄露连接被快速回收前池子就已耗尽。场景三希望记录泄露但不立即回收用于观察在某些调试阶段你可能想看看有多少泄露发生但又不想影响当前正在执行的、可能超时的业务。变通方案保持remove-abandoned: true和log-abandoned: true。将remove-abandoned-timeout-millis设置为一个非常大的值例如1小时或更长。这样泄露连接会在日志中被记录通过log-abandoned但不会在短时间内被强制回收方便你收集足够的堆栈信息进行分析而不会立即引发回滚异常。4. 代码层面如何从根本上杜绝连接泄露配置只是治标写出健壮的代码才是治本。再好的回收机制也只是最后的保险丝。我们应该追求的是代码本身不泄露任何连接。4.1 最佳实践使用 try-with-resources这是 Java 7 以来处理必须关闭的资源如Connection,Statement,ResultSet的首选和最强力推荐的方式。它能保证在代码块执行完毕后无论是否发生异常资源都会被自动关闭。// 传统方式容易忘记关闭或在异常处理中漏关 Connection conn null; Statement stmt null; ResultSet rs null; try { conn dataSource.getConnection(); stmt conn.createStatement(); rs stmt.executeQuery(SELECT * FROM user); // ... 处理结果 } catch (SQLException e) { // 处理异常 } finally { // 需要手动、反向关闭非常繁琐且易错 if (rs ! null) try { rs.close(); } catch (SQLException ignore) {} if (stmt ! null) try { stmt.close(); } catch (SQLException ignore) {} if (conn ! null) try { conn.close(); } catch (SQLException ignore) {} } // 现代方式使用try-with-resources绝对安全 try (Connection conn dataSource.getConnection(); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(SELECT * FROM user)) { // ... 处理结果 } catch (SQLException e) { // 处理异常 } // 无需finally块连接、语句、结果集会在try块结束时自动、正确地关闭。在 Spring MyBatis 环境中我们通常不直接操作Connection而是通过SqlSession或Mapper接口。确保你获取的SqlSession在正确的范围内被关闭。在 Spring 管理的事务中这通常是自动完成的。4.2 框架使用规范MyBatis 与 Spring 事务1. 确保在 Service 层使用Transactional业务操作应该在 Service 层方法上声明事务。这样Spring 会帮你管理连接的获取、提交/回滚和归还。避免在 DAO/Mapper 层手动控制事务。Service public class UserService { Autowired private UserMapper userMapper; Transactional // Spring 会在此方法开始时获取连接方法结束时根据是否异常决定提交或回滚并归还连接。 public void updateUser(User user) { userMapper.update(user); // 其他数据库操作... } }2. 警惕“连接穿透”不要在 Service 方法中将DataSource或Connection对象传递给其他非受 Spring 事务管理的方法或线程这会导致连接生命周期管理混乱。3. 正确处理SqlSession如果你需要手动管理SqlSession例如在非 Web 环境的工具类中必须使用try-with-resources或确保在 finally 块中关闭。try (SqlSession session sqlSessionFactory.openSession()) { UserMapper mapper session.getMapper(UserMapper.class); mapper.insert(user); session.commit(); // 手动提交 } // session 会自动关闭连接归还给连接池4.3 利用logAbandoned堆栈信息定位问题当你在日志中看到 Druid 打印的泄露警告时类似下面这样WARN - com.alibaba.druid.pool.DruidDataSource - abandon connection, owner thread: http-nio-8080-exec-5, connected at: 1672531200000, open stackTrace at com.example.YourService.yourMethod(YourService.java:50) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1080) ...重点看at com.example.YourService.yourMethod(YourService.java:50)这一行及其附近的调用栈。这行代码就是最初借出连接但没有归还的地方。你需要检查这个方法是否有分支逻辑如if/else,return提前退出导致连接没有走到关闭逻辑是否在循环中获取连接但只在循环外关闭了一次是否在异步回调或子线程中使用了连接但没有正确管理其生命周期根据堆栈信息去审查对应代码十有八九能找到泄露的根源。5. 进阶源码浅析与问题深度规避对于想更深入了解的同学我们可以简单看看 Druid 相关源码这能加深理解并在遇到更复杂问题时提供思路。removeAbandoned的核心逻辑在DruidDataSource类的removeAbandoned()方法中。它会遍历activeConnections活跃连接列表对每个连接检查其lastActiveTime和connectedTime。如果(当前时间 - connectedTime) removeAbandonedTimeoutMillis并且连接处于空闲状态则将其加入待回收列表。一个重要的注意点是Druid 在回收连接时会先调用conn.rollback()。这意味着如果你的业务代码在连接泄露期间修改了数据但未提交这些修改会被 Druid 强制回滚这可能导致数据丢失。因此绝不能依赖超时回收来代替正确的事务提交。深度规避建议代码审查与静态分析将“连接泄露”作为代码审查的重点项。可以使用 SonarQube 等静态代码分析工具它们能检测出未关闭的资源Resource not closed这类问题。集成测试编写集成测试模拟长时间运行或异常退出的场景然后检查测试结束后连接池的状态可以通过 Druid 的监控界面或 JMX确保没有连接泄露。监控与告警持续监控生产环境 Druid 连接池的ActiveCount、RemoveAbandonedCount等关键指标。如果RemoveAbandonedCount持续大于0说明系统存在持续的连接泄露需要立即告警并排查。Druid 自带的stat-view-servlet和Spring Boot Actuator集成是很好的监控手段。配置好removeAbandoned机制就像是给系统加装了一个烟雾报警器。它不能防止火灾代码bug但能在火势蔓延连接池耗尽之前发出警报甚至自动启动喷淋回收连接。真正的安全来自于规范的代码编写和严谨的资源管理习惯。希望这篇从异常现象到源码原理再到实战配置和代码规范的长文能帮你彻底驯服数据库连接泄露这只“房间里的大象”。下次再看到connection closed你就能从容地把它变成优化系统稳定性的一个契机了。