JDBC 数据库访问与连接池全过程
JDBC 是 Java 访问关系型数据库的标准 API,不是数据库网络协议。应用面向 Connection、PreparedStatement、ResultSet 编程,MySQL/Oracle/PostgreSQL 等驱动把这些调用转换成各自数据库协议。MyBatis、JPA、Hibernate、Spring JDBC 最终仍要取得 JDBC Connection、执行语句、处理结果和结束事务。
学习目标
- 讲清 Driver 如何发现、DriverManager/DataSource 如何取得连接;
- 理解物理连接建立、认证、TLS、会话状态和数据库协议的边界;
- 正确选择 Statement、PreparedStatement、CallableStatement 和执行方法;
- 掌握 ResultSet 游标、NULL、类型映射、Fetch Size 和大结果集;
- 理解 autoCommit、commit、rollback、Savepoint、隔离级别和 Connection 绑定;
- 正确执行批处理和获取生成主键;
- 理解连接池借出代理、close 归还、状态重置、泄漏检测和容量约束;
- 区分连接超时、查询超时、网络超时、事务超时和未知提交结果;
- 能定位 SQL 注入、慢 SQL、连接池耗尽、事务未提交、连接失效和重试重复写。
一、JDBC 在完整链路中的位置
flowchart TD
A["业务Service"] --> B["MyBatis JPA或Spring JDBC"]
B --> C["JDBC标准接口"]
C --> D["数据库厂商JDBC Driver"]
D --> E["TCP TLS和数据库协议"]
E --> F["数据库连接会话"]
F --> G["SQL解析优化执行"]
G --> H["结果行或影响行数"]
H --> D
D --> C
C --> B
B --> AJDBC 统一 Java API,但不会让所有数据库 SQL 方言、隔离行为、批处理和流式读取完全一致。跨库代码仍要验证驱动版本与数据库行为。
二、Driver 是怎样被加载的
2.1 JDBC 4 之前的常见写法
Class.forName("com.mysql.jdbc.Driver");加载类触发驱动静态初始化并向 DriverManager 注册。现代 MySQL 驱动类名是 com.mysql.cj.jdbc.Driver,但 JDBC 4 通常不再需要手动 Class.forName。
2.2 JDBC 4 SPI 自动发现
驱动 JAR 在:
META-INF/services/java.sql.Driver声明实现类。DriverManager 初始化时通过服务发现加载可见驱动。完整概念链:
flowchart TD
A["驱动JAR进入ClassPath"] --> B["META-INF services声明Driver"]
B --> C["DriverManager触发SPI发现"]
C --> D["实例化并注册Driver"]
D --> E["按JDBC URL询问acceptsURL"]
E --> F["匹配驱动建立连接"]“No suitable driver” 常见原因:驱动 JAR 不在运行时 ClassPath、URL 前缀错误、类加载器不可见、javax/模块环境差异或驱动初始化失败。编译通过不能证明部署环境一定能发现驱动。
2.3 Web 应用类加载泄漏
驱动由 WebAppClassLoader 加载并注册到全局 DriverManager,应用重部署时若未注销,可能持有旧类加载器。现代容器和连接池通常协助清理,但自定义生命周期仍要检查 Driver、线程和连接池是否关闭。
三、DriverManager 与 DataSource
3.1 DriverManager
Connection connection = DriverManager.getConnection(
"jdbc:mysql://db:3306/order_db",
"app_user",
"secret");适合教学、工具和简单程序。它根据 URL 选择 Driver,并直接创建连接。账号密码不能硬编码或写日志,应由 Secret 管理并最小权限授权。
3.2 DataSource
Connection connection = dataSource.getConnection();DataSource 是连接工厂抽象,可表示普通数据源、连接池或支持 XA 的数据源。业务代码不需要知道物理连接如何创建、复用、故障检测和统计。生产项目通常注入 DataSource。
| 维度 | DriverManager | DataSource |
|---|---|---|
| 配置 | 调用处传 URL/属性 | 集中配置/注入 |
| 池化 | 本身不提供通用池管理 | 实现可提供连接池 |
| 监控 | 较弱 | 常有 active、idle、wait 等 |
| 事务集成 | 手工 | Spring/JTA 可管理 |
| 生产推荐 | 特殊工具 | 常规业务首选 |
四、创建物理连接到底做了什么
不同数据库协议不同,但常见过程包括:
flowchart TD
A["解析主机和端口"] --> B["建立TCP连接"]
B --> C["可选TLS握手与证书校验"]
C --> D["数据库协议握手"]
D --> E["身份认证"]
E --> F["协商字符集时区和能力"]
F --> G["创建数据库会话"]
G --> H["Connection可执行SQL"]创建连接昂贵不仅是 new 一个 Java 对象,还涉及网络往返、认证、数据库会话内存和可能的 TLS。连接池复用的是已经建立的物理连接。
Connection 还携带会话状态:autoCommit、隔离级别、只读标记、catalog/schema、网络超时、临时表、会话变量等。连接归还池时必须恢复,避免下一个借用者继承脏状态。
五、Connection 的真实语义
Connection 表示一个数据库会话,事务通常绑定在这条 Connection 上:
Connection A 执行更新1
Connection A 执行更新2
Connection A commit才可能属于同一事务。若两条 SQL 分别从池中借不同 Connection,即使业务方法相邻,也不是同一本地事务。
JDBC Connection、Statement、ResultSet 通常不应被多个业务线程并发使用。连接池的并发安全是“多个线程安全借还不同连接”,不是“同一 Connection 可随意多线程执行”。
5.1 try-with-resources 关闭顺序
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql);
ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
// 映射当前行
}
}资源按声明逆序关闭:ResultSet → Statement → Connection。即使后面的 close 抛异常,try-with-resources 会保留主异常并把关闭异常放入 suppressed exceptions;排查时可查看 getSuppressed()。
六、Statement、PreparedStatement、CallableStatement
| 类型 | SQL 形式 | 典型用途 |
|---|---|---|
| Statement | 完整 SQL 字符串 | 固定 DDL/管理语句,谨慎使用 |
| PreparedStatement | SQL 模板 + 参数 | 绝大多数查询和更新 |
| CallableStatement | 存储过程/函数调用 | 强依赖数据库的过程接口 |
6.1 SQL 注入为什么发生
错误代码:
String sql = "select id from users where name='" + username
+ "' and password='" + password + "'";用户输入成为 SQL 语法的一部分,可闭合引号、追加条件或联合查询。不要靠手工替换单引号,编码、注释和数据库方言仍可能绕过。
6.2 PreparedStatement 参数为什么更安全
String sql = "select id, name from users where name = ?";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, username);
try (ResultSet rs = ps.executeQuery()) {
// ...
}
}SQL 结构和参数值通过驱动协议分离处理,输入被当作值而不是 SQL 语法。是否使用服务端真正预编译取决于驱动和配置;即使驱动采用客户端模拟绑定,只要正确实现参数编码,仍能防止参数逃逸成 SQL 结构。
6.3 哪些内容不能用问号参数化
表名、列名、ASC/DESC、SQL 关键字通常不能用 ? 作为值绑定:
select ? from ? order by ? ?动态标识符必须从服务端白名单映射:
String orderBy = "createdAt".equals(sort)
? "created_at" : "id";
String direction = "asc".equalsIgnoreCase(dir) ? "ASC" : "DESC";
String sql = "select id from orders order by " + orderBy + " " + direction;用户输入不能直接拼接,即使使用 PreparedStatement 执行。
6.4 类型绑定与 setObject
优先使用明确类型:setString/setLong/setBigDecimal/setTimestamp。无类型 setObject(null) 在部分驱动中类型推断不稳定,可使用 setNull(index, Types.VARCHAR)。金额使用 BigDecimal,不能用 double;时间要明确数据库类型、时区和驱动配置。
七、三类 execute 方法
| 方法 | 预期结果 | 返回 |
|---|---|---|
executeQuery() | 单个 ResultSet | ResultSet |
executeUpdate() | INSERT/UPDATE/DELETE/DDL | 影响行数 int |
execute() | 结果类型未知或多结果 | 首个结果是否为 ResultSet |
executeUpdate() 返回 0 不一定是失败:条件未匹配、值未变化、DDL 或驱动语义都可能影响返回值。业务乐观锁通常明确要求影响行数等于 1,否则视为版本冲突。
存储过程可能返回多个 ResultSet 和更新计数,需要循环 getMoreResults/getUpdateCount,并及时关闭结果;普通 CRUD 不要为了通用而滥用 execute。
八、ResultSet 游标和类型映射
ResultSet 初始游标位于第一行之前,必须先 next():
flowchart TD
A["executeQuery返回ResultSet"] --> B["游标在第一行之前"]
B --> C{"调用next还有行吗"}
C -- "是" --> D["游标移动到当前行"]
D --> E["按列读取并映射"]
E --> C
C -- "否" --> F["遍历结束并关闭"]8.1 SQL NULL 与 Java 基本类型
int age = rs.getInt("age");
if (rs.wasNull()) {
// 数据库 age 实际是 NULL,而不是 0
}getInt/getLong 对 SQL NULL 返回 0,需要紧接着 wasNull();对象方法可返回 null。不能把数据库 NULL 与业务 0 混在一起。
8.2 按列名还是下标
按下标略直接但对 SELECT 列顺序敏感;按别名更可读。不要 select * 后依赖隐式顺序,显式列出字段、别名和映射,减少表结构变化影响和无用网络数据。
8.3 ResultSet 类型
默认通常是 TYPE_FORWARD_ONLY、CONCUR_READ_ONLY。可滚动、可更新 ResultSet 需要驱动和数据库支持,成本更高且可移植性差。业务查询通常使用前向只读流并在 Java 中构建结果。
九、大结果集、Fetch Size 与流式读取
setFetchSize 是给驱动的提示,不保证所有驱动以同样方式分批。部分驱动默认把全部结果缓存在客户端;MySQL 流式读取在不同 Connector/J 版本可能需要特殊 fetchSize、useCursorFetch、服务端预处理等配置,必须按驱动官方文档验证。
PreparedStatement ps = connection.prepareStatement(
sql,
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY);
ps.setFetchSize(500);
ps.setQueryTimeout(30);流式处理仍要:
- SQL 有稳定索引与分页/游标条件;
- 每行立即处理,不能又全部加入 List;
- 处理逻辑不能过慢,否则长时间占连接和数据库快照;
- 设置查询/网络/事务超时;
- 异常时关闭 ResultSet/Statement/Connection;
- 避免单事务扫描千万行导致 undo/WAL、MVCC 版本保留和复制压力。
大批量导出通常使用基于主键的 Keyset Pagination 或数据库导出能力,而不是 offset 深分页和 readAll。
十、JDBC 事务全过程
10.1 autoCommit=true
新 Connection 默认通常为自动提交。每条 SQL 执行完成后成为独立事务:
UPDATE订单 自动提交
UPDATE库存 自动提交若第二条失败,第一条已经提交,不能 rollback 回来。因此多语句业务必须显式关闭自动提交或交给 Spring 事务管理。
10.2 手工事务模板
Connection connection = null;
try {
connection = dataSource.getConnection();
connection.setAutoCommit(false);
updateOrder(connection);
updateInventory(connection);
connection.commit();
} catch (Exception e) {
if (connection != null) {
try {
connection.rollback();
} catch (SQLException rollbackError) {
e.addSuppressed(rollbackError);
}
}
throw e;
} finally {
if (connection != null) {
try {
connection.close();
} catch (SQLException closeError) {
// 记录连接归还失败,不能覆盖主异常
}
}
}10.3 完整状态过程
flowchart TD
A["借出Connection"] --> B["setAutoCommit false"]
B --> C["执行SQL 1"]
C --> D["执行SQL 2"]
D --> E{"全部成功吗"}
E -- "是" --> F["commit"]
E -- "否" --> G["rollback"]
F --> H["恢复连接状态并close归还"]
G --> H不要在 catch 中吞掉异常后让上层认为成功。rollback 也可能失败,例如网络已断;必须保留原异常和 rollback 异常。
10.4 setAutoCommit 的隐式提交边界
JDBC 规范下,把 autoCommit 从 false 改为 true 会提交当前事务;不要把它当普通布尔字段随意切换。某些数据库 DDL 还会隐式提交,具体按数据库验证。连接归还池前由池恢复 autoCommit,不代表可以漏掉业务 commit/rollback。
10.5 Connection close 时事务会怎样
不能依赖 close 自动 rollback 作为业务逻辑。规范/驱动/池可能有不同处理和告警,显式 commit 或 rollback 后再归还,才能让结果可推理。
十一、隔离级别与只读属性
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
connection.setReadOnly(true);JDBC 常量:
| 常量 | 语义 |
|---|---|
TRANSACTION_READ_UNCOMMITTED | 可能脏读 |
TRANSACTION_READ_COMMITTED | 防脏读,快照/锁细节由数据库决定 |
TRANSACTION_REPEATABLE_READ | 同事务重复读语义更强 |
TRANSACTION_SERIALIZABLE | 接近串行效果,冲突/阻塞成本高 |
JDBC 只提供请求隔离级别的 API,MySQL MVCC、PostgreSQL 快照和 Oracle 行为并不完全相同。设置前用 supportsTransactionIsolationLevel 或数据库文档确认。
setReadOnly(true) 通常是提示或路由依据,不是安全权限。防止写入必须依靠数据库只读账号、事务/数据库配置和权限控制。
十二、Savepoint 局部回滚
connection.setAutoCommit(false);
Savepoint savepoint = connection.setSavepoint("after_order");
try {
insertOptionalCoupons(connection);
} catch (SQLException e) {
connection.rollback(savepoint);
}
connection.commit();Savepoint 允许回滚到事务内某点,不代表创建嵌套独立事务。回滚到保存点后,保存点之前的修改仍在当前未提交事务中;外层最终 rollback 会全部撤销。数据库和驱动对 DDL、保存点释放等有差异。
十三、批处理全过程
String sql = "insert into order_item(order_id, sku_id, quantity) values (?, ?, ?)";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
int batchSize = 500;
for (int i = 0; i < items.size(); i++) {
OrderItem item = items.get(i);
ps.setLong(1, item.getOrderId());
ps.setLong(2, item.getSkuId());
ps.setInt(3, item.getQuantity());
ps.addBatch();
if ((i + 1) % batchSize == 0) {
ps.executeBatch();
ps.clearBatch();
}
}
ps.executeBatch();
}批处理减少网络往返和解析开销,但不是批次越大越好:参数占内存、SQL 包大小、锁持有时间、事务日志、复制延迟和失败重试范围都会扩大。根据压测选择 100/500/1000 等批量,不要一次提交百万行。
BatchUpdateException#getUpdateCounts() 可提供执行到何处的线索,但不同驱动的继续/停止策略不同。若事务要求全有全无,发生任何批次异常都 rollback 整个事务;重试必须有业务唯一键和幂等设计。
十四、生成主键
PreparedStatement ps = connection.prepareStatement(
"insert into orders(biz_no, amount) values (?, ?)",
Statement.RETURN_GENERATED_KEYS);
ps.setString(1, bizNo);
ps.setBigDecimal(2, amount);
int affected = ps.executeUpdate();
if (affected != 1) throw new SQLException("insert failed");
try (ResultSet keys = ps.getGeneratedKeys()) {
if (!keys.next()) throw new SQLException("missing generated key");
long id = keys.getLong(1);
}主键返回支持和批量键顺序依赖驱动。分布式系统还可能使用业务 ID/雪花 ID,不能把数据库自增值暴露成可猜测权限凭证。
十五、连接池为什么 close 不是关闭物理连接
flowchart TD
A["DataSource.getConnection"] --> B["池选择空闲物理连接"]
B --> C["创建或返回代理Connection"]
C --> D["业务执行SQL和事务"]
D --> E["业务调用proxy.close"]
E --> F["池回收并校验连接"]
F --> G["回滚未结束事务并重置状态"]
G --> H["连接进入idle队列"]业务看到的 Connection 常是代理。close() 标记代理不可再用,并把底层物理连接归还池;池关闭或连接失效时才真正关闭 Socket。
归还时池通常重置:autoCommit、readOnly、isolation、catalog/schema、networkTimeout 等,并清理警告。池不能可靠清理所有数据库会话副作用,例如临时表、自定义会话变量和锁;业务应避免污染共享连接。
十六、连接池容量不是越大越好
数据库能并行处理的连接受 CPU、磁盘、锁、buffer、最大连接数和查询成本限制。池从 20 调到 500 可能让数据库上下文切换、锁竞争和缓存抖动更严重。
用 Little 定律做初步容量理解:
并发占用连接数 ≈ 数据库请求吞吐 × 平均持有连接时间例如每秒 200 个数据库操作,平均持有 50ms:
200 × 0.05 = 10 个平均并发连接还要为峰值和尾延迟留余量,并受数据库总连接预算约束。多个应用实例总池上限是每实例池大小之和,扩容实例会同步增加潜在数据库连接。
十七、连接池关键参数和指标
| 指标/参数 | 含义 | 风险信号 |
|---|---|---|
| maximumPoolSize | 最大连接数 | 过大压垮DB,过小排队 |
| minimumIdle | 最小空闲 | 过高长期占DB连接 |
| connectionTimeout | 借连接等待超时 | 超时上涨说明池饱和 |
| idleTimeout | 空闲回收 | 与最小空闲共同理解 |
| maxLifetime | 连接最大寿命 | 应与DB/代理超时错开并加抖动 |
| validation | 连接存活校验 | 过重校验增加延迟 |
| leakDetection | 长时间未归还提示 | 可能慢SQL或真正泄漏 |
| active/idle/pending | 使用、空闲、等待 | pending持续增长是告警 |
泄漏检测阈值过短会把正常慢事务误报为泄漏。日志中的“借出堆栈”是证据起点,不代表一定忘记 close,也可能是 SQL/下游在持有连接期间太慢。
十八、超时不是一个参数
| 超时 | 控制阶段 | 常见入口 |
|---|---|---|
| DNS/TCP connect timeout | 建立数据库网络连接 | Driver URL/属性 |
| pool connectionTimeout | 等待池中可用连接 | 连接池配置 |
| login timeout | 登录/建连上限 | DataSource/DriverManager,驱动支持有差异 |
| query timeout | 单条 Statement 执行 | setQueryTimeout(seconds) |
| network timeout | Connection 网络读写 | setNetworkTimeout(executor, ms) |
| transaction timeout | 整个事务预算 | Spring/JTA/业务 deadline |
| socket timeout | 驱动网络读取 | 驱动属性 |
Query timeout 通常通过驱动取消查询,取消不是绝对瞬时;数据库端可能仍在处理,网络中断也可能让结果未知。上层事务超时不自动代替驱动网络超时。
18.1 Statement.cancel
另一个线程可调用 statement.cancel() 尝试取消,但驱动可能通过额外连接/协议消息实现,不能保证所有阶段都立即响应。取消后连接是否仍可复用要由池/驱动验证。
十九、SQLException 怎样读
catch (SQLException e) {
System.err.println("sqlState=" + e.getSQLState());
System.err.println("vendorCode=" + e.getErrorCode());
System.err.println("message=" + e.getMessage());
SQLException next = e.getNextException();
}- SQLState 是标准化分类线索,例如以
08开头常表示连接异常; - vendorCode 是数据库厂商错误码;
- nextException 在批处理/多错误中很重要;
- SQLException 子类包括 transient/non-transient、recoverable、integrity constraint 等,但驱动映射质量不同。
日志记录 SQL 模板、耗时、数据库标识、traceId 和错误码,不能记录密码、完整连接串、敏感参数。定位时以数据库慢日志、错误日志和驱动异常链共同为证据。
二十、网络故障和“提交结果未知”
最危险场景:应用发送 COMMIT,数据库已经提交,但响应在网络中丢失。应用只看到连接异常,无法仅凭异常判断事务是否提交。
flowchart TD
A["应用发送COMMIT"] --> B["数据库完成持久化"]
B --> C["返回成功响应"]
C --> D["网络在响应到达前断开"]
D --> E["应用得到连接异常"]
E --> F["业务结果处于未知状态"]此时直接重试“创建订单/扣款”可能重复执行。治理:业务唯一请求号、唯一索引、幂等状态机、按业务号查询最终结果和补偿。不要对所有 SQLException 无脑重试;死锁/序列化冲突可在幂等边界内有限退避重试,语法错误和约束错误重试无意义。
二十一、完整商业事务 Demo
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import javax.sql.DataSource;
public class OrderRepository {
private final DataSource dataSource;
public OrderRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
public long createOrder(String bizNo, BigDecimal amount, long skuId)
throws SQLException {
Connection connection = null;
try {
connection = dataSource.getConnection();
connection.setAutoCommit(false);
int inventoryRows;
try (PreparedStatement inventory = connection.prepareStatement(
"update inventory set stock=stock-1 "
+ "where sku_id=? and stock>0")) {
inventory.setLong(1, skuId);
inventoryRows = inventory.executeUpdate();
}
if (inventoryRows != 1) {
throw new SQLException("insufficient stock");
}
long orderId;
try (PreparedStatement order = connection.prepareStatement(
"insert into orders(biz_no,amount,status) values(?,?,?)",
Statement.RETURN_GENERATED_KEYS)) {
order.setString(1, bizNo);
order.setBigDecimal(2, amount);
order.setString(3, "CREATED");
if (order.executeUpdate() != 1) {
throw new SQLException("order insert failed");
}
try (ResultSet keys = order.getGeneratedKeys()) {
if (!keys.next()) throw new SQLException("missing order id");
orderId = keys.getLong(1);
}
}
connection.commit();
return orderId;
} catch (SQLException e) {
if (connection != null) {
try { connection.rollback(); }
catch (SQLException rollbackError) { e.addSuppressed(rollbackError); }
}
throw e;
} finally {
if (connection != null) {
try { connection.close(); }
catch (SQLException closeError) {
// 使用日志框架记录,不要覆盖主异常
}
}
}
}
}biz_no 应有唯一索引以支撑幂等。库存条件更新把“检查库存和扣减”合成单条原子 SQL,不能先 SELECT 再无条件 UPDATE。真实 Spring 项目通常由 @Transactional 管理 Connection 绑定和提交回滚,但 SQL、锁、连接池和未知结果问题仍存在。
二十二、连接泄漏是怎样发生的
flowchart TD
A["请求从池借Connection"] --> B["执行SQL或业务"]
B --> C{"所有路径都close吗"}
C -- "是" --> D["归还池"]
C -- "否" --> E["active连接不下降"]
E --> F["idle连接耗尽"]
F --> G["新请求等待连接"]
G --> H["connectionTimeout和线程堆积"]泄漏来源:异常分支提前 return、手工关闭遗漏、异步线程跨边界持有 Connection、ResultSet 流式处理未结束、事务方法卡在远程调用、连接代理被缓存到成员字段。
连接持有时间过长和真正泄漏表现相似。必须结合借出堆栈、连续 active/pending、SQL 状态和线程栈判断。
二十三、生产故障排查 Runbook
23.1 连接池耗尽
- 查看 active、idle、pending、acquire timeout、max pool;
- 判断 active 是否长期等于 max;
- 获取连续 jstack,找等待
getConnection和持有连接线程; - 持有者停在 SQL 则查数据库活动会话/慢日志/锁;
- 停在 HTTP/MQ 则说明事务内做慢外部调用;
- 检查 leak detection 借出堆栈;
- 比较请求 TPS、SQL P99、事务持有时长和数据库容量;
- 不要第一反应只增大池。
23.2 查询慢
记录 SQL 模板、参数类型、行数、耗时和数据库;在数据库用真实参数分布 EXPLAIN/执行计划,检查索引、估算行、回表、排序、临时表、锁等待和统计信息。Java 线程栈只能证明在等 JDBC,不能证明是 CPU 执行慢还是锁阻塞。
23.3 事务未提交或长事务
检查 autoCommit、commit 分支、异常是否吞掉、Spring 事务是否生效、是否自调用绕过代理、连接是否被错误切换。数据库侧查看事务开始时间、锁、undo/WAL/MVCC 版本保留;长事务通常来自事务中远程调用、大循环或等待用户输入。
23.4 频繁出现失效连接
对比数据库/防火墙/NAT idle timeout 与连接池 maxLifetime/keepalive;让池在基础设施强制断开前主动退休连接,并使用抖动避免同时重连。检查数据库重启、主从切换、DNS 和证书。不要每次借连接都执行昂贵验证 SQL。
23.5 内存暴涨
检查是否 select * 大结果集全部装 List、驱动是否客户端全量缓冲、fetchSize 是否真正生效、LOB 是否读入 byte[]、批次过大。使用堆分析看行对象、byte[]、驱动缓存;改为流式/Keyset/分批并限制导出。
23.6 重试后数据重复
确认异常发生在 SQL 前、执行中还是 commit 响应丢失;使用业务请求号和唯一索引查询最终状态。只对明确可重试错误做有限退避,事务整体重跑时每个外部副作用也必须幂等。
二十四、常见错误与后果
| 错误 | 后果 | 正确方向 |
|---|---|---|
| 把 JDBC 说成数据库协议 | 无法理解驱动差异 | JDBC是API,Driver转协议 |
| 字符串拼接参数 | SQL注入 | PreparedStatement绑定值 |
| 用户输入直接拼表名/排序 | 仍然注入 | 服务端白名单映射 |
| 默认autoCommit执行多步业务 | 部分成功无法回滚 | 同Connection显式事务 |
| close前不commit/rollback | 结果依赖池/驱动 | 显式结束事务 |
| Connection存单例成员共享 | 并发事务串扰 | 每工作单元借出并归还 |
| 事务内调用慢HTTP | 长期占连接和锁 | 重构边界/Outbox等 |
| 连接池无限调大 | DB竞争和雪崩 | 基于容量压测 |
| 全部SQLException无脑重试 | 重复写、持续失败 | 分类、幂等、有限退避 |
| 大查询全部装List | OOM和长占连接 | 流式/分页/导出任务 |
二十五、JDK 与 JDBC 版本边界
| 能力 | JDK 7 | JDK 8 | 后续版本 |
|---|---|---|---|
| try-with-resources | 引入,可用于JDBC | 支持 | 持续增强 |
| JDBC 4.1 | JDK 7 | 可用 | - |
| JDBC 4.2 | - | JDK 8,增强java.time/SQL类型支持基础 | 后续继续增强 |
| Lambda | 无 | 有,但JDBC核心API不依赖 | 有 |
JDK 8 的 java.time 与 JDBC 4.2 支持仍取决于驱动版本;老驱动可能不支持直接 setObject(LocalDateTime)。企业 JDK 7/8 项目必须核对数据库驱动兼容矩阵,不能只升级驱动而不做协议、时区和行为回归。
二十六、面试标准回答
JDBC 完整执行流程
驱动通过 JDBC 4 SPI 被发现,DataSource/池借出 Connection;应用创建 PreparedStatement、绑定类型参数,驱动把调用编码为数据库协议;数据库解析优化执行并返回行或影响数;应用遍历 ResultSet,在同一 Connection 上 commit/rollback,最后逆序关闭并把代理 Connection 归还池。
PreparedStatement 一定服务端预编译吗
不一定,取决于驱动和配置,可能使用服务端预处理,也可能客户端编码后发送。但正确参数绑定仍把值与 SQL 结构分离,能防止参数变成 SQL 语法。表名、列名、排序方向不能作为普通参数,必须白名单。
Connection close 到底做什么
非池化连接通常关闭物理数据库连接;池化 DataSource 返回的常是代理,close 把连接归还池并触发回滚未完成事务、状态重置和健康检查。业务仍必须在 close 前显式 commit/rollback。
连接池为什么不是越大越好
数据库并行能力有限,过多连接增加上下文切换、锁竞争、内存和磁盘压力。池大小要结合数据库总连接预算、实例数、请求吞吐和连接持有时间压测,扩容应用实例还会放大总连接数。
连接异常后可以直接重试事务吗
不能。COMMIT 可能已在数据库成功而响应丢失,此时结果未知,直接重试会重复写。必须有业务请求号、唯一约束和状态查询,只对明确瞬态错误在幂等边界内有限退避重试。
二十七、关联知识与验收
掌握验收:
- 能画出 Driver SPI、DataSource、Connection、数据库协议和执行结果链;
- 能解释参数绑定为何防注入,以及标识符为何不能用问号;
- 能写包含影响行校验、生成主键、commit、rollback 和 suppressed 异常的事务;
- 能解释 ResultSet 初始游标、SQL NULL 和 wasNull;
- 能说明池化 Connection close、状态重置和会话污染;
- 能区分借连接、查询、网络和事务超时;
- 面对连接池耗尽,能从 pending、持有线程、慢SQL、锁和外部调用建立证据链;
- 能解释 COMMIT 响应丢失为什么是未知结果,以及为什么必须业务幂等。
