读完这篇,你能做什么
能编写参数化查询并归还资源,说明池耗尽与真实数据库验证的边界。
学习理由、准备要求与知识点
为什么学它
标准数据库访问与资源管理机制是理解连接池、MyBatis 和事务框架的桥梁。
先读这些内容
知识点清单
- Connection
/Statement/ResultSet PreparedStatement参数绑定- SQL 结构白名单
DataSource与驱动- 连接借用和归还
- 池容量和等待超时
- SQL 与网络超时区别
- 同连接事务边界
运行前先确认Java 17 JDBC,源码选 HikariCP 5.1.0 ProxyConnection,补充 MySQL Connector/J 9.0.0 参数绑定路径。算法只分析离线借用时间区间,不连接数据库,不以某次区间峰值宣称生产最优池大小。
01先弄懂它是什么
一次 JDBC 查询主要涉及三个对象:Connection 代表数据库会话,PreparedStatement 保存 SQL 模板和参数,ResultSet 用于读取结果。DataSource 负责提供连接,但它的实现不一定带连接池;数据库驱动把这些标准调用转换成对应数据库协议。看清它们各自做什么,才能理解 MyBatis 和事务框架替你管理了哪些步骤。
查询商户编号时,在 SQL 中写问号,再用 setLong 传入值,可以把数据和 SQL 结构分开,避免拼接用户文本。表名、列名和排序方向属于结构,通常不能靠普通参数占位替代,应从服务端允许的选项中选择。PreparedStatement 也不保证底层每次都走同一种服务端预编译方式,具体取决于驱动和配置。
连接池主要减少反复建立物理连接的成本,并对同时占用的资源形成容量边界。应用借到的通常是包装连接,close 表示结束本次借用并让池决定复用或淘汰,而不是每次都关闭数据库 TCP 连接。不调用 close 不会更高效,只会让其他请求等不到资源。
文中这几个词,先认一认
- 逻辑连接
- 应用本次借到的连接包装对象,close 结束这次借用后,就不应再使用它。
- 物理连接
- 真正维持数据库会话的底层连接,可以由连接池在多次借用之间重复使用。
- 池等待
- SQL 还没执行,应用先在等连接池空出可用连接的这段时间。
- 连接状态污染
- 上一次使用留下的事务、只读等设置没恢复,导致下一次借用受到影响。
02看看它怎么工作
资源获取后应形成明确作用域:先取连接,再创建语句,再读取结果,关闭时按相反顺序释放。try-with-resources 表达这种嵌套关系,异常时也能触发关闭。结果集使用结束不代表连接已归还,尤其不能把连接存入静态字段让多个请求交叉使用同一事务状态。
关闭池连接前需要考虑未完成事务、未关闭语句和已修改的连接属性。自动提交关闭后发生写入却既未提交也未回滚,不能把这份未决状态直接交给下一请求。池会按自身机制清理和重置,但应用仍应明确事务成功提交、失败回滚,不能把池的最后防御当正常业务控制流。
请求总耗时包含获取连接、执行语句、读取结果和应用持有期间的其他工作。假如 SQL 只用十毫秒,但事务中又等远程服务一秒,连接会被长时间占用。调大池可能让数据库承受更多竞争而不是消除等待;应先测量借用时长、活动数与等待数,再定位慢 SQL、泄漏或过大的事务范围。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["请求借用连接"] --> B{"池有可用资源"}
B -->|"否"| C["等待或获取超时"]
B -->|"是"| D["创建语句并绑定值"]
D --> E["执行并读取结果"]
E --> F["明确提交或回滚"]
F --> G["关闭结果与语句"]
G --> H["关闭逻辑连接"]
H --> I["清理状态并复用或淘汰物理连接"]按顺序看,每一步做了什么
- 使用实际
DataSource获取本次连接并确定谁负责归还。 - 绑定值后执行语句,检查查询结果或影响行数。
- 明确事务边界,异常路径不能遗漏回滚或关闭。
- 按阶段观察等待、执行和持有时间,再决定容量或代码调整。
03对照实现代码
HikariCP 5.1.0 · com.zaxxer.hikari.pool.ProxyConnection#close、markCommitStateDirty
已核验 close 的语句关闭、未决事务回滚、dirtyBits 重置和 finally 回收路径。下例为原创保守模型,任何清理失败都标记不可复用;真实 HikariCP 还有错误分类和淘汰逻辑,不能宣称逐行等价。
class SourceModel {
interface Physical { void closeStatements(); void rollback(); void reset(); }
interface Pool { void release(Physical connection,boolean reusable); }
static class Handle implements AutoCloseable {
final Physical physical; final Pool pool;
boolean closed=false,transactionDirty=false,autoCommit=true;
Handle(Physical physical,Pool pool) { this.physical=physical; this.pool=pool; }
public void close() {
if(closed) return; // 本次逻辑句柄重复关闭不重复归还
boolean reusable=false;
try {
physical.closeStatements(); // 先处理仍打开的下级资源
if(transactionDirty && !autoCommit) physical.rollback();
physical.reset(); reusable=true; // 清理成功才允许本模型复用
} finally {
closed=true; pool.release(physical,reusable); // 关闭句柄与回收物理资源分开
}
}
}
}真实 close 会先关闭它记录下来的语句对象;这一步如果出错,连接本身也可能被标记为损坏。接着,代理再检查自己是否仍关联底层连接,而不是已经被标成关闭。因此 Connection.close 不只是把一个对象放回空闲列表,前面还有按顺序完成的资源清理和状态检查。
若存在未决提交状态且不是自动提交,代理会执行回滚;修改过的连接属性根据 dirtyBits 恢复,之后清理警告。它记录了哪些状态被应用改变,以减少不必要的重置,但状态追踪依赖通过正确的 API 使用连接。绕过包装器修改底层对象可能破坏这些假设。
finally 会把应用手中的代理设为已关闭,再把池条目交回池管理。应用若继续使用旧连接对象,应收到关闭错误,不能悄悄操作已经借给别人的底层连接。真实池是否复用,还要看连接有没有被淘汰;教学模型把失败都按不可复用处理,是为便于解释采用的保守规则。
Connector/J 的 setInt 等方法进入查询参数绑定对象,说明参数值与语句结构在客户端内部也有明确管理。连接池不验证业务 SQL 的正确性,不修复缺索引,也不会把不同连接中的写入自动组成同一事务。源码阅读要沿连接、语句、参数和事务分别定位责任。
代码先调用谁,再调用谁
- 应用取得池包装连接并创建受跟踪语句。
- 执行操作时记录提交和属性状态变化。
ProxyConnection.close处理下级资源与状态恢复。- 代理变为关闭,池条目按健康和淘汰状态决定后续复用。
04动手写一个例子
从连接借用区间计算实际并发峰值
给定连接借用区间 [start,end),求同时占用的最大连接数及首次达到该峰值的时间。时间非负,end 不小于 start,零长度区间忽略;空输入返回峰值0和时间-1。区间应表示实际借用,不把等待连接的时间混进去。
- 输入 1
[0,5),[5,9)- 输出 1
峰值1,首次时间0
- 输入 2
[0,10),[1,4),[2,6)- 输出 2
峰值3,首次时间2
- 输入 3
[5,5)- 输出 3
峰值0,时间-1
- 输入 4
end 小于 start- 输出 4
IllegalArgumentException
每个非空区间拆成开始的加一事件和结束的减一事件。按时间排序后扫描活动计数,最大值就是观测到的占用峰值。不能把所有请求数直接当连接数,因为持有时间是否重叠才决定同时需求。
区间采用左闭右开,同一时刻先归还的连接可以立即再次借出。因此排序时同一时间先处理减一,再处理加一,避免把无缝衔接误算成额外并发。比较时间使用 Long.compare,而不是相减后强转,防止极值溢出。
首次超过旧峰值时记录时间,之后相等不改变。这个结果只描述给定观测,不直接给出未来最优池容量。main 覆盖衔接、重叠、空输入、零长度、相同开始时间、长整型极值与非法区间。
为什么这样写是对的处理完某个时间点的全部事件后,active 等于包含该时刻的借用区间数;peak 是此前所有已处理状态的最大值。
n 个区间产生至多 2n 个事件,排序主导时间,扫描线性。事件集合与排序空间按线性规模计。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.*;
public class Main {
record Lease(long start,long end) {
Lease { if(start<0 || end<start) throw new IllegalArgumentException(); }
}
record Event(long at,int delta) {}
record Peak(int count,long firstTime) {}
static Peak peak(List<Lease> leases) {
if(leases==null) throw new IllegalArgumentException();
List<Event> events=new ArrayList<>();
for(Lease lease:leases) {
if(lease==null) throw new IllegalArgumentException();
if(lease.start()==lease.end()) continue; // 零长度不占用资源
events.add(new Event(lease.start(),1)); events.add(new Event(lease.end(),-1));
}
events.sort(Comparator.comparingLong(Event::at).thenComparingInt(Event::delta));
int active=0,max=0; long when=-1;
for(Event e:events) {
active+=e.delta(); // 同时刻先减后加,符合左闭右开区间
if(active>max) { max=active; when=e.at(); } // 只记录首次新峰值
if(active<0) throw new AssertionError("事件序列不应出现负占用");
}
return new Peak(max,when);
}
static Lease l(long a,long b) { return new Lease(a,b); }
static void eq(Peak expected,Peak actual) { if(!expected.equals(actual)) throw new AssertionError(actual); }
public static void main(String[] args) {
eq(new Peak(1,0),peak(List.of(l(0,5),l(5,9))));
eq(new Peak(3,2),peak(List.of(l(0,10),l(1,4),l(2,6))));
eq(new Peak(0,-1),peak(List.of()));
eq(new Peak(0,-1),peak(List.of(l(5,5))));
eq(new Peak(2,0),peak(List.of(l(0,1),l(0,2))));
eq(new Peak(2,Long.MAX_VALUE-1),peak(List.of(l(0,Long.MAX_VALUE),l(Long.MAX_VALUE-1,Long.MAX_VALUE))));
try { l(4,3); throw new AssertionError("应拒绝反向区间"); }
catch(IllegalArgumentException expected) { /* 错误边界 */ }
System.out.println("PASS 7 lease interval tests");
}
}这些特殊情况也要试一试
- 同一时刻结束与开始不重叠。
- 未归还连接没有完整结束时间,需要额外标注截尾观测,不能默认为零时长。
- 真实系统的时间来源应一致,跨主机时间偏差会影响区间重叠判断。
进阶选读 · 生产拓展:给定容量的排队模拟
场景希望比较不同池容量下的等待时间,而不仅看历史活动峰值。
做法给定到达时刻和假设占用时长,用最小堆维护各连接最早空闲时间,模拟有限容量下的开始与结束,计算等待分布。再用真实实验核对时长是否随数据库并发变化。
复杂度按到达排序后,n 个任务在容量 c 下可用 O(n log c) 堆操作模拟;延迟分位数的精确排序另需 O(n log n)。
好处与代价固定时长模型可比较排队机制,却忽略数据库竞争使任务变慢的反馈,不能作为未经实测的最优容量结论。
05项目里怎么用
池容量应与数据库可承受并发、事务占用时长和业务峰值一起讨论。全系统多实例的池上限会相加,不能只看单实例配置。增加应用副本却保持每副本大池,可能让数据库连接与锁竞争一起上升。
连接获取超时、语句执行超时与网络读取超时分别发生在不同阶段,日志应保留 SQLState、阶段和关联标识,同时对参数脱敏。泄漏检测告警说明借用过久,不一定证明永久泄漏,还需检查长事务或阻塞调用。
Spring 管事务时,数据库访问也要采用能使用它所管理连接的方式。在事务方法里另外开连接并自行提交,不会仅因方法带注解就自动加入原事务。把连接使用、异常传出和最终提交围绕同一个业务操作安排清楚。
容易踩的坑
- 为了复用而不 close 逻辑连接,最终耗尽池。
- 把
DataSource接口名当作已经启用连接池的证据。 - 在持有事务连接期间执行长时间不可控远程等待。
- 认为参数绑定能把表名、列名或授权规则一并解决。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 较小有界池 | 限制数据库并发,便于形成背压。 | 高峰可能增加等待或拒绝。 | 数据库容量有限且有明确请求预算。 |
| 较大池 | 在数据库确有余量时可能减少应用等待。 | 可能增加锁、CPU、内存和连接管理竞争。 | 经同条件测量证明存在收益的负载。 |
| 缩短连接占用范围 | 不增加数据库连接也能提升复用效率。 | 需要梳理事务与远程调用顺序。 | 连接持有明显长于必要数据库工作的用例。 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01为什么池连接 close 后物理连接仍可能存在?
展开参考答案与推导
close 结束当前逻辑借用,池可以清理后复用健康底层会话。应用不再拥有旧句柄,物理生命周期由池管理。
02PreparedStatement 是否保证 SQL 永远在服务端预编译?
展开参考答案与推导
不能仅凭接口推断。参数化 API 与具体驱动协议实现不同,客户端或服务端准备方式由驱动版本和配置决定。
03池等待与慢 SQL 为什么要分开?
展开参考答案与推导
等待连接发生在执行前,可能由其他请求长期占用引起。只看单条 SQL 时长无法解释整个请求延迟。
换个情况,还会处理吗
01活动连接长期满但数据库 CPU 很低,如何查?
展开参考答案与推导
检查连接借用栈、长事务、远程等待和未归还资源,同时看数据库锁等待。低 CPU 不代表没有资源阻塞,也不支持立即扩池。
02请求 A 修改只读状态后,请求 B 行为异常,怀疑什么?
展开参考答案与推导
核对连接状态是否通过正确包装 API 修改和恢复,是否绕过池或共享了同一连接。连接复用需要清理状态,不仅是复用一个对象。
03历史峰值只有 10,是否可以断言池设 10 最优?
展开参考答案与推导
不能。历史可能已被容量限制,未来到达分布和占用时间不同,数据库并发反馈也未体现。它是观测证据,需要结合等待与错误目标继续验证。
写代码试一试
01为什么同一时刻归还和借出要先处理归还?
- 输入 / 条件
[0,5),[5,9)- 输出 / 预期
峰值1
提示 按左闭右开定义判断包含关系。
展开参考答案与推导
时刻5第一个区间已经结束,第二个才开始,不存在同时占用;排序减一在加一之前避免制造瞬间假峰值。
02如何标注尚未归还的连接?
- 输入 / 条件
开始10,观测窗口结束20,未收到结束事件- 输出 / 预期
至少占用到20,并标记未完成
提示 不能伪造真实归还时间。
展开参考答案与推导
用观测窗口截尾参与当前占用分析,另存未完成标识;后续证据到达后再更新,不能把截尾当正常释放。
03给池句柄模型测试重复 close。
- 输入 / 条件
对同一句柄关闭两次- 输出 / 预期
只调用一次归还
提示 逻辑关闭标志属于句柄生命周期。
展开参考答案与推导
使用计数 Pool 替身记录 release 次数,首次 close 后标记关闭,再次调用直接返回;并发误用是否支持需单独声明与同步。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能编写参数化查询并归还资源,说明池耗尽与真实数据库验证的边界。
本篇重点加练
重点增加连接借用生命周期与分阶段超时的练习,帮助定位连接耗尽的真实来源。
- 连接借用和归还
- SQL 与网络超时区别
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
为按学生编号查询课程画出 Connection、
PreparedStatement、ResultSet的创建与关闭关系,标出值绑定发生的位置,并定义无记录和多记录的合同。- 提交内容
- 资源图、SQL 与绑定参数表。
- 怎样算完成
- 资源所有者和释放时机明确
- 结果集生命周期不越过连接合同
练到的知识点Connection/Statement/ResultSet · PreparedStatement 参数绑定
- 作业 02设计评审
查询允许用户选择 name 或
created_at排序,同时数据库可能切换驱动实现。分别设计 SQL 结构入口与DataSource创建边界。- 提交内容
- 排序规则和连接抽象图。
- 怎样算完成
- 结构输入受明确定义约束
- 驱动与池的职责区分
练到的知识点SQL 结构白名单 · DataSource 与驱动
综合应用
- 作业 03排错实验
本地测试池最大连接数设为 2,依次借用两个连接并发起第三次借用,再归还一个;记录等待、超时和恢复过程,所有句柄最终关闭。
- 提交内容
- 隔离实验、自写超时和归还断言、运行记录。
- 怎样算完成
- 池等待与数据库执行分开
- 测试资源完整清理
练到的知识点连接借用和归还 · 池容量和等待超时
- 作业 04面试场景
一次请求设置事务后执行两次更新,中途网络阻塞;另一个请求在池中等待。定义需要区分的超时与事务状态证据,说明从异常消息无法直接得出的结论。
- 提交内容
- 阶段时间线、指标与诊断表。
- 怎样算完成
- 借用等待、SQL 和网络阶段区分
- 提交结果未知有独立处理合同
练到的知识点SQL 与网络超时区别 · 同连接事务边界
- 作业 05编程实践
用 JDBC 实现练习账户间转账,限制同一连接完成读写并明确失败回滚;测试正常、余额不足、第二次更新注入失败,查询实际持久化状态。
- 提交内容
- Java 代码、隔离建表脚本、自写边界及失败测试、运行证据。
- 怎样算完成
- 资源和事务状态均恢复
- 测试在事务外核对账本
- 参数绑定不拼接金额或编号
练到的知识点Connection/Statement/ResultSet · PreparedStatement 参数绑定 · 连接借用和归还 · 同连接事务边界
- 作业 06排错实验
在本地受控连接池中分别模拟“长时间借出不执行 SQL”和“短借用但慢查询”,采集借用等待、执行与归还时间;资源数量和运行时间均设上限。
- 提交内容
- 实验、自写资源清理检查和分阶段时间记录。
- 怎样算完成
- 两种等待来源可区分
- 失败后池状态得到核验
练到的知识点连接借用和归还 · SQL 与网络超时区别
- 作业 07设计评审
导出接口把
ResultSet交给调用者逐行处理,连接何时归还尚未定义。为正常结束、提前取消和处理抛错分别制定资源所有权合同。- 提交内容
- 资源时序图与取消验收。
- 怎样算完成
- 调用者和提供者责任明确
- 提前结束不会被遗漏
练到的知识点连接借用和归还
- 作业 08面试场景
设置 SQL 超时后仍可能在获取连接阶段等待很久。制定总请求预算的拆分与监测方案,说明每类超时发生时已占用什么资源。
- 提交内容
- 预算表和故障定位图。
- 怎样算完成
- 超时设置绑定具体阶段
- 端到端完成时间另有判据
练到的知识点SQL 与网络超时区别
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
完成性能测量后:某接口平均连接持有 50ms,突发请求量明显增加。设计池容量实验,记录排队、超时、数据库负载与尾延迟,避免仅按历史峰值选择。
- 提交内容
- 容量实验计划与停止条件。
- 怎样算完成
- 负载和资源预算可比
- 池扩容不会被默认视作所有故障修复
练到的知识点连接借用和归还 · 池容量和等待超时 · SQL 与网络超时区别
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 条件变化
接口从一次查询改为边查边等待远程服务,每批数据间隔不确定。审查连接持有范围与超时政策,比较吞吐、内存和一致性取舍。
- 提交内容
- 两种处理方案及资源账本。
- 怎样算完成
- 连接占用与外部等待关系明确
- 选型依据包含实际业务快照需求
练到的知识点连接借用和归还 · SQL 与网络超时区别
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读教程英文
Oracle JDBC:PreparedStatement (新标签页)↗
练习参数绑定、查询和更新;教程采用早期 JDK,标准 JDBC 原则仍适用于 Java 17。
- 实践参考文档英文
Java 17 API:DataSource (新标签页)↗
区分普通数据源与连接池实现,阅读取得连接的标准接口。
- 深入参考文档英文
HikariCP 官方项目:配置说明 (新标签页)↗
阅读
maximumPoolSize、connectionTimeout、maxLifetime与关闭连接的归还语义;版本按项目依赖锁定。 - 深入参考文档英文
MySQL 8.4:提交与回滚 (新标签页)↗
阅读自动提交和显式事务,区分 DML 回滚与 DDL 隐式提交。
- 深入参考文档英文
HikariCP 5.1.0 ProxyConnection (新标签页)↗
核对 close 清理、回滚、状态恢复和回收逻辑。
- 深入参考文档英文
Connector/J 9.0.0 ClientPreparedStatement (新标签页)↗
定位
setInt等参数进入驱动绑定对象的路径。