JDBC、DataSource 与连接池:一条 SQL 如何借到可靠连接
接口偶发拿到只读连接、事务已经结束却仍占着池、数据库没有慢 SQL 而请求都在超时——这些现象经常被归咎于连接池参数。真正的问题往往更早:应用没有说清连接是谁的、借出后改了什么状态、在哪个 deadline 内必须归还。
数据访问的第一条硬边界不是 Mapper 或 Repository,而是 JDBC Connection。事务隔离级别、auto-commit、read-only、网络超时与当前事务都挂在连接上;池化以后,close() 通常只是把逻辑句柄归还池,而不是关闭物理 socket。借出者若改变状态却没有恢复,下一个请求就会继承不属于自己的会话语义。Oracle JDBC 教程把 DataSource 作为取得连接的首选入口,接口契约则应回到 Java SE 的 java.sql API 核对。
一次借还包含两层连接
应用拿到的 Connection 往往是代理。代理保存借出时间、池条目和关闭标记,真正的物理连接持有驱动会话与 socket。调用 close 后,代理先阻止继续使用,再回滚遗留事务,恢复 autoCommit、readOnly、isolation、catalog、schema、networkTimeout 等被修改状态,清理 warning,最后才把物理连接重新放回 idle 队列。不同连接池的复位集合与优化方式不同,应用不能依赖“池一定替我修复所有会话变量”。
池大小不是“越大越快”。应用线程、池连接、数据库工作进程与磁盘/CPU 是串联容量;连接超过数据库可并行执行能力后,只会把等待搬到数据库内部并放大锁竞争。池的职责是限制并发、复用建连成本并给等待设上界。用到达率、数据库驻留时间和目标利用率估算起点,再用池等待分布、数据库活跃会话和吞吐拐点校准。
connectionTimeout 限制借连接等待,validationTimeout 限制存活检查,statement/query timeout 限制语句,network timeout 限制驱动网络操作,事务 deadline 限制整个工作单元。它们必须单调收敛:内层先到期,外层保留回滚和错误写回时间。HikariCP 的 validationTimeout 必须小于 connectionTimeout;leakDetectionThreshold 只是“借出过久”的诊断信号,不是回收连接的安全机制,也不能替代 finally/try-with-resources。
连接泄漏要区分三种现象:代码路径没有 close;事务或查询本来就长;线程阻塞导致 finally 迟迟无法执行。泄漏日志只能指出借出栈,不能证明根因。证据要同时查看 active/idle/pending、最老借出年龄、线程栈、数据库会话状态与事务年龄。强行回收仍在使用的连接可能让提交结果变成未知,因此正常池不应把 leak detection 当作 kill switch。
连接失效同样不是一个布尔值。数据库重启、代理 idle timeout、NAT 回收和网络半开都会留下“客户端认为可用”的旧 socket。借出前测试每条连接会增加固定成本;完全不验证又会把失效暴露给业务请求。池通常结合驱动 isValid、keepalive、maxLifetime 与失败驱逐。maxLifetime 应避开基础设施的硬回收边界并加入抖动,防止连接同时退休造成建连风暴。
池耗尽时先分清“连接太少”还是“连接不回来”
active 等于 maximumPoolSize 只是表象。若数据库活跃语句很少而大量连接处于 idle in transaction,说明事务边界或异常路径没有结束;若数据库语句都在锁等待,扩池会把更多事务送进同一等待图;若线程栈卡在结果集消费或远程调用,说明连接跨越了不应覆盖的业务步骤。只有数据库仍有并行余量、连接都在有效执行且池等待稳定存在时,增加池容量才可能提升吞吐。
连接池还会与应用线程池形成双队列。请求先在线程池等待,再在连接池等待,两个超时各自计时会让端到端延迟超过 SLO。更糟的是任务持有连接后再提交到同一饱和线程池等待子任务,形成资源互等。数据访问应保持单向所有权:执行线程在有限作用域借连接,完成数据库工作后立即归还,不把 Connection、ResultSet 或惰性流交给异步消费者。
上线基线要同时压测冷启动和稳定态。冷启动可能集中创建连接、做 TLS/认证和预编译;稳定态则观察复用、退休与 keepalive。数据库故障恢复测试要验证旧连接被驱逐、新连接创建受到速率限制、等待请求在 deadline 内失败,以及恢复后 active/idle 回到基线,而不是只验证下一次健康检查变绿。
连接池:不是越大越好
MySQL 慢不一定是 SQL 慢,也可能是应用连接池排队。后端开发至少要理解连接池几个参数。
以 HikariCP 为例:
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 10
connection-timeout: 3000
validation-timeout: 1000
idle-timeout: 600000
max-lifetime: 1700000这里的数字不是通用答案。你要按服务实例数和数据库最大连接数计算:
总连接上限 = 单实例 maximum-pool-size * 应用实例数 + 任务实例连接数 + 管理连接预留如果数据库允许 300 个连接,线上有 10 个应用实例,每个实例连接池开 50,理论上就可能打到 500 个连接,还没算批量任务。这个时候调大连接池不是解决问题,而是在制造更大的排队和数据库压力。
验证方式:
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Threads_running';
SHOW VARIABLES LIKE 'max_connections';应用侧要看连接池等待指标、活跃连接数、空闲连接数和超时次数。生产建议是:核心服务、后台任务、报表任务使用独立线程池和连接池配置,避免导出任务把在线接口连接耗尽。
容量增长:别等表大了再设计边界
容量问题往往在需求阶段就埋下了。一个列表接口如果允许任意时间范围、任意状态组合、任意排序、任意页码,数据增长后一定会出事。
开发时至少做一次容量估算:
日订单量:100 万
保留在线查询热数据:180 天
热表行数:约 1.8 亿
单用户最大订单:10 万
核心列表页:按 user_id + status + created_at 倒序
后台报表:按天汇总,不走在线明细表实时聚合然后把边界写进接口契约:
普通用户列表只查当前用户,默认最近 90 天,最大 180 天。管理端查询必须带时间范围,单次导出走异步任务。报表统计走汇总表或离线任务,不在在线明细表上做无限聚合。
大批量更新按 id 范围分批,每批有上限和间隔。
常见坑是只讨论“当前数据量能不能跑”。架构师要问的是“按这个增长速度,半年后核心查询是否还能解释”。
用两个状态模型拆掉框架错觉
下面两个 Java 17 程序只保留本篇最关键的状态与分支。它们不连接真实数据库,因此不能证明驱动或数据库的厂商行为;它们用来证明调用方必须维持的不变量,真实集成测试再负责验证 SQL、锁和网络。
javac --release 17 -Xlint:all -Werror examples/backend-development/data-access/jdbc-datasource-pool/PoolLeaseResetDemo.java examples/backend-development/data-access/jdbc-datasource-pool/PoolTimeoutBudgetDemo.java
java -cp examples/backend-development/data-access/jdbc-datasource-pool PoolLeaseResetDemo
java -cp examples/backend-development/data-access/jdbc-datasource-pool PoolTimeoutBudgetDemophysicalId=7 autoCommit=true readOnly=false returnedToPool=true
deadlineMs=800 remainingMs=50 admitted=true poolTimeoutBounded=true输出的价值在于固定中间状态,而不是展示 API 能运行。修改实现后,如果资源没有复位、冲突被误报为成功、缓存跨越了更新边界或调用身份发生变化,模型应先失败,随后真实数据库测试再给出厂商级证据。
从“SQL 慢”退回第一处状态偏差
一次数据访问至少要能关联:事务或工作单元 ID、数据源与路由结果、连接获取等待、statement 标识、规范化 SQL 指纹、批量行数、执行时间、返回或影响行数、异常 SQLState/厂商码、提交结果和连接归还结果。参数值默认不进入指标标签;日志也必须对白名单字段脱敏。原始 SQL 文本、租户、主键和游标会制造高基数或敏感信息泄漏,应进入受控 trace 事件而不是常驻指标。
排障顺序从池等待开始,而不是直接索引调优。没有拿到连接时,数据库慢查询里不会出现证据;已经进入数据库但没有返回,需要结合活动语句、锁等待、执行计划与网络;结果已经返回而接口仍慢,则检查对象物化、N+1、序列化和事务提交。最终异常可能是连接关闭失败,第一处偏差却可能是业务语句超时,因果链不能被最后一个堆栈覆盖。
生产验收使用趋势不变量:同负载下池等待年龄不单调增长,事务结束后 borrowed 连接回到稳定基线,statement 超时早于外层 deadline,批量失败可定位到子批次,提交结果未知不会自动重放非幂等写入,升级前后 SQL 指纹、影响行数和查询计划差异可审查。阈值来自容量预算和基线测量,不从教程复制万能数字。
