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/jdbc 或 mybatis 实现,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 分层」的学习范围组织,正文与示例均为本站原创整理。