← 返回 Java 后端知识路线
阶段 03数据库与持久化

连接池、DbUtils 与 DAO:把数据库资源交给可观察的边界

从 DriverManager 的单次连接升级到连接池和 DAO,理解借出、归还、映射与资源泄漏之间的关系。

第 19 / 35 篇
连接池HikariCPDbUtilsDAO资源管理

先看这一课值不值得学

学完后,你手里多了哪些代码积木

复用昂贵连接,并把持久化职责收进 DAO。

本课正式新增

语法 / API / 命令你必须会到什么程度
DataSource从连接池借出 Connection
QueryRunner用 DbUtils 减少 JDBC 样板
DAO method以业务对象为输入输出封装 SQL

本课只借用,先别硬背

  • Spring Repository 注入到第 25、30 课再接入

学完必须能独立写

  • 实现文章 DAO 的增删改查
  • 保证连接按借出、使用、归还完整闭环
本课目录
  1. 1. 现实问题:每次请求新建连接会把数据库拖垮
  2. 2. 最小可运行示例:DataSource 借用与 DAO 查询
  3. 3. 调用链与对象变化
  4. 4. 为什么这样设计
  5. 5. 项目落点:连接池指标和 DAO 责任
  6. 6. 易错排查
  7. 7. 一页复习

1. 现实问题:每次请求新建连接会把数据库拖垮

DriverManager 每次调用都可能建立 TCP、认证和会话,延迟高且连接数不可控。把 Connection 做成一个全局变量又会让多个请求互相覆盖事务和游标。连接池把有限连接预先创建并借出,但它不是“数据库变快了”,而是把资源生命周期、上限和等待变成可配置对象。

DAO 还会重复写 prepare、绑定、遍历和关闭模板。Apache DbUtils 可以减少样板,但它不能替你决定 SQL、事务、映射语义和异常策略。抽象的目标是集中资源管理,不是隐藏所有数据库行为。

2. 最小可运行示例:DataSource 借用与 DAO 查询

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

public class ArticleDao {
    private final DataSource dataSource;
    public ArticleDao(DataSource dataSource) { this.dataSource = dataSource; }

    public List<String> titlesByStatus(String status) throws Exception {
        String sql = "SELECT title FROM article WHERE status = ? ORDER BY id DESC LIMIT 20";
        List<String> titles = new ArrayList<>();
        try (Connection connection = dataSource.getConnection();
             PreparedStatement statement = connection.prepareStatement(sql)) {
            statement.setString(1, status);
            try (ResultSet rows = statement.executeQuery()) {
                while (rows.next()) titles.add(rows.getString(1));
            }
        }
        return List.copyOf(titles);
    }
}

上面的 DataSource 可以由 HikariCP 或容器提供,DAO 不需要知道连接是新建还是池中借出的。调用 connection.close() 的语义在池里通常是归还而非物理销毁,所以仍然必须调用。DbUtils 的 QueryRunner 也应该接收 DataSource,并在每次操作后正确关闭资源。

3. 调用链与对象变化

DAO 调用 DataSource.getConnection,池检查 active 数量,找到空闲连接后把代理 Connection 借给调用方。prepare 和 ResultSet 的链路与 JDBC 相同;try 块结束时 close 触发归还,池重置自动提交、隔离级别、只读状态和警告,防止下一个请求继承前一个请求的状态。

DAO 返回的是新建的 List 和字符串值,池不会把数据库实体对象缓存给调用方。连接池内部维护队列、最大连接数、超时和健康检查;借不到连接时会等待或抛异常,这个错误比数据库无限创建连接更容易观察和保护。

4. 为什么这样设计

连接是昂贵且有限的外部资源,池让并发上限显式化。池太大可能压垮数据库,太小会让请求排队;应根据数据库 max_connections、请求耗时、事务长度和实例数共同估算。连接池参数不是复制网上配置,而是负载测试后的运行参数。

DAO 隔离 SQL 与业务,让 Service 不知道 ResultSet 游标;DbUtils 适合简单行映射,但复杂嵌套对象需要明确 ResultSetHandler 或手写 Mapper。过度通用的 BaseDao 常把表差异和事务边界藏掉,遇到复杂查询反而更难排查。

5. 项目落点:连接池指标和 DAO 责任

Boot 项目通常通过 DataSource 自动配置连接池,Service 不直接调用 getConnection,事务边界由事务管理器或明确的 Repository API 负责。监控 active、idle、pending、获取连接耗时;日志带 request id 和 SQL 模板名,不打印密码。

练习:给 DAO 增加一个故意不关闭连接的测试,在短连接池中循环调用并观察超时;修复后再断言所有连接归还。使用 Testcontainers 或独立 MySQL 测试 schema 做集成测试,避免把开发库数据当测试夹具。

6. 易错排查

  • 连接池耗尽:检查未关闭、长事务、慢查询和 pool size,不要只盲目增大连接数。
  • 归还脏状态:手动设置 autoCommit、隔离级别、readOnly 后确认池会重置。
  • DbUtils 隐藏异常:保留 SQL 场景和 cause,必要时为关键映射写显式 handler。
  • DAO 返回内部可变列表:用不可变副本,避免上层修改结果影响缓存或后续逻辑。

7. 一页复习

DataSource 是资源入口,连接池控制借还和上限,DAO 封装 SQL 与映射,DbUtils 只减少模板不替代设计。关闭逻辑连接就是归还池,事务和连接状态必须在边界内清理。把连接池指标纳入可观察性,才能判断系统是慢查询还是资源等待。

连接池借还可以写成一条状态机:请求进入时等待空闲连接,成功后连接被标记 active,DAO 在事务内执行,finally 关闭逻辑连接,池重置状态并放回 idle;超过 acquisition timeout 仍借不到就失败。active 很高不一定是池太小,也可能是慢 SQL、长事务或线程持有连接去调用外部网络,必须同时看 query latency 和 pending。

DataSource 配置应落在 Boot 配置文件和环境变量,代码只注入接口;最大池、最小空闲、连接超时、验证查询和泄漏检测要有依据。DbUtils 的 QueryRunner 可以统一 execute/query 模板,但 RowProcessor 仍需知道列名、null 和枚举;复杂嵌套映射不要为了少写几行而塞进一个通用 BeanUtils。

项目文件按责任拆开:persistence/ArticleRepository 声明接口,persistence/jdbcmybatis 实现,config/DataSourceConfig 管资源,application 管事务用例。DAO 返回 DTO/记录而不是泄漏 Connection,Service 不应该在循环中自行借连接。指标记录池 active、idle、pending、获取耗时、泄漏警告和数据库错误。

排错时池耗尽先查未关闭和长事务,再查 max pool 是否超过数据库容量;连接状态污染查 autoCommit、readOnly、隔离级别是否被恢复;偶发超时查网络和数据库锁等待,不要只提升 timeout;DbUtils 映射为空查列别名和 handler。练习是配置一个很小的测试池,故意不关闭一次连接,等待超时后修复并用日志证明归还。

连接池的核心不是“多几个 Connection”,而是把稀缺资源的等待、借出、使用、归还和失效都显式化。请求进入时会等待空闲连接,借出后连接可能处于事务、只读或特定隔离级别,close 触发的是归还和状态重置。若一个请求在持有连接时调用外部 API,池的 active 会长时间不降,后续请求可能在 pending 队列中超时。

池参数要和数据库容量、实例数量和事务平均时长一起估算。最大池乘以应用实例数不能超过数据库可用连接,连接获取超时应小于 HTTP 请求超时,泄漏检测只用于发现异常不能当正常流程。指标应能区分“没有空闲连接”“数据库慢”“网络断开”和“线程没有释放”,否则调大池只会把故障推到数据库。

DbUtils 的 QueryRunner 能封装 prepare、execute 和关闭,但它的输入仍然是 SQL、参数和 Handler,输出仍然是行映射结果。SimpleBeanProcessor 可能按属性名映射失败,复杂一对多要自定义 handler;空值、列别名、枚举和时间精度应写测试。通用 BaseDao 如果把事务和所有表差异藏起来,遇到批处理、锁或特殊查询反而更难证明。

项目文件把 DataSource 配置放 config,Repository 接口放 application,Jdbc/DbUtils 实现在 persistence,事务方法放 Service。Service 只拿到领域结果,不拿 Connection;Repository 只在调用边界借还资源。连接池日志要带 pool name、active、idle、pending 和 borrow time,不能把账号和 URL 密码写入日志。

排查连接耗尽先查关闭路径、长事务和慢查询,再查 pool size;连接归还后行为异常查 autoCommit/readOnly/隔离状态有没有恢复;偶发 mapping 错查 handler 和列别名;数据库拒绝新连接查实例总池数和 max_connections。练习是配置四个连接的测试池,制造五个并发慢查询,观察等待、超时、日志和修复后归还数量。

验证清单:用四个连接的测试池同时发起五个慢查询,记录每个请求的 borrow 开始、获得连接、SQL 返回、逻辑 close 和最终状态;第五个请求应进入等待或按超时失败,而不是无限挂起。故意在事务中改变 autoCommit、readOnly 和隔离级别,执行完后借下一次连接检查状态已恢复。再让 DbUtils 的 handler 面对列别名、null 时间和未知枚举,确认映射错误带表/列上下文。复盘时把 Connection 留在 DAO 内,把 DTO 带回 Service,验证事务和资源边界没有向 Controller 泄漏。

进阶附录:池化与虚拟线程的关系

虚拟线程可以承载更多阻塞任务,但数据库连接仍是有限池资源;任务数量增加后,等待连接的队列可能变长。线程模型、连接池大小和数据库吞吐要一起测,不能因为线程变轻就把池无限放大。

本课按「JDBC 连接池、Apache DbUtils 与 DAO 分层」的学习范围组织,正文与示例均为本站原创整理。