Skip to content

JDBC 数据库访问与连接池全过程

JDBC 是 Java 访问关系型数据库的标准 API,不是数据库网络协议。应用面向 ConnectionPreparedStatementResultSet 编程,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 在完整链路中的位置

mermaid
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 --> A

JDBC 统一 Java API,但不会让所有数据库 SQL 方言、隔离行为、批处理和流式读取完全一致。跨库代码仍要验证驱动版本与数据库行为。

二、Driver 是怎样被加载的

2.1 JDBC 4 之前的常见写法

java
Class.forName("com.mysql.jdbc.Driver");

加载类触发驱动静态初始化并向 DriverManager 注册。现代 MySQL 驱动类名是 com.mysql.cj.jdbc.Driver,但 JDBC 4 通常不再需要手动 Class.forName。

2.2 JDBC 4 SPI 自动发现

驱动 JAR 在:

text
META-INF/services/java.sql.Driver

声明实现类。DriverManager 初始化时通过服务发现加载可见驱动。完整概念链:

mermaid
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

java
Connection connection = DriverManager.getConnection(
        "jdbc:mysql://db:3306/order_db",
        "app_user",
        "secret");

适合教学、工具和简单程序。它根据 URL 选择 Driver,并直接创建连接。账号密码不能硬编码或写日志,应由 Secret 管理并最小权限授权。

3.2 DataSource

java
Connection connection = dataSource.getConnection();

DataSource 是连接工厂抽象,可表示普通数据源、连接池或支持 XA 的数据源。业务代码不需要知道物理连接如何创建、复用、故障检测和统计。生产项目通常注入 DataSource。

维度DriverManagerDataSource
配置调用处传 URL/属性集中配置/注入
池化本身不提供通用池管理实现可提供连接池
监控较弱常有 active、idle、wait 等
事务集成手工Spring/JTA 可管理
生产推荐特殊工具常规业务首选

四、创建物理连接到底做了什么

不同数据库协议不同,但常见过程包括:

mermaid
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 上:

text
Connection A 执行更新1
Connection A 执行更新2
Connection A commit

才可能属于同一事务。若两条 SQL 分别从池中借不同 Connection,即使业务方法相邻,也不是同一本地事务。

JDBC Connection、Statement、ResultSet 通常不应被多个业务线程并发使用。连接池的并发安全是“多个线程安全借还不同连接”,不是“同一 Connection 可随意多线程执行”。

5.1 try-with-resources 关闭顺序

java
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/管理语句,谨慎使用
PreparedStatementSQL 模板 + 参数绝大多数查询和更新
CallableStatement存储过程/函数调用强依赖数据库的过程接口

6.1 SQL 注入为什么发生

错误代码:

java
String sql = "select id from users where name='" + username
        + "' and password='" + password + "'";

用户输入成为 SQL 语法的一部分,可闭合引号、追加条件或联合查询。不要靠手工替换单引号,编码、注释和数据库方言仍可能绕过。

6.2 PreparedStatement 参数为什么更安全

java
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 关键字通常不能用 ? 作为值绑定:

sql
select ? from ? order by ? ?

动态标识符必须从服务端白名单映射:

java
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()单个 ResultSetResultSet
executeUpdate()INSERT/UPDATE/DELETE/DDL影响行数 int
execute()结果类型未知或多结果首个结果是否为 ResultSet

executeUpdate() 返回 0 不一定是失败:条件未匹配、值未变化、DDL 或驱动语义都可能影响返回值。业务乐观锁通常明确要求影响行数等于 1,否则视为版本冲突。

存储过程可能返回多个 ResultSet 和更新计数,需要循环 getMoreResults/getUpdateCount,并及时关闭结果;普通 CRUD 不要为了通用而滥用 execute。

八、ResultSet 游标和类型映射

ResultSet 初始游标位于第一行之前,必须先 next()

mermaid
flowchart TD
    A["executeQuery返回ResultSet"] --> B["游标在第一行之前"]
    B --> C{"调用next还有行吗"}
    C -- "是" --> D["游标移动到当前行"]
    D --> E["按列读取并映射"]
    E --> C
    C -- "否" --> F["遍历结束并关闭"]

8.1 SQL NULL 与 Java 基本类型

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_ONLYCONCUR_READ_ONLY。可滚动、可更新 ResultSet 需要驱动和数据库支持,成本更高且可移植性差。业务查询通常使用前向只读流并在 Java 中构建结果。

九、大结果集、Fetch Size 与流式读取

setFetchSize 是给驱动的提示,不保证所有驱动以同样方式分批。部分驱动默认把全部结果缓存在客户端;MySQL 流式读取在不同 Connector/J 版本可能需要特殊 fetchSize、useCursorFetch、服务端预处理等配置,必须按驱动官方文档验证。

java
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 执行完成后成为独立事务:

text
UPDATE订单  自动提交
UPDATE库存  自动提交

若第二条失败,第一条已经提交,不能 rollback 回来。因此多语句业务必须显式关闭自动提交或交给 Spring 事务管理。

10.2 手工事务模板

java
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 完整状态过程

mermaid
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 后再归还,才能让结果可推理。

十一、隔离级别与只读属性

java
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 局部回滚

java
connection.setAutoCommit(false);
Savepoint savepoint = connection.setSavepoint("after_order");
try {
    insertOptionalCoupons(connection);
} catch (SQLException e) {
    connection.rollback(savepoint);
}
connection.commit();

Savepoint 允许回滚到事务内某点,不代表创建嵌套独立事务。回滚到保存点后,保存点之前的修改仍在当前未提交事务中;外层最终 rollback 会全部撤销。数据库和驱动对 DDL、保存点释放等有差异。

十三、批处理全过程

java
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 整个事务;重试必须有业务唯一键和幂等设计。

十四、生成主键

java
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 不是关闭物理连接

mermaid
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 定律做初步容量理解:

text
并发占用连接数 ≈ 数据库请求吞吐 × 平均持有连接时间

例如每秒 200 个数据库操作,平均持有 50ms:

text
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 timeoutConnection 网络读写setNetworkTimeout(executor, ms)
transaction timeout整个事务预算Spring/JTA/业务 deadline
socket timeout驱动网络读取驱动属性

Query timeout 通常通过驱动取消查询,取消不是绝对瞬时;数据库端可能仍在处理,网络中断也可能让结果未知。上层事务超时不自动代替驱动网络超时。

18.1 Statement.cancel

另一个线程可调用 statement.cancel() 尝试取消,但驱动可能通过额外连接/协议消息实现,不能保证所有阶段都立即响应。取消后连接是否仍可复用要由池/驱动验证。

十九、SQLException 怎样读

java
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,数据库已经提交,但响应在网络中丢失。应用只看到连接异常,无法仅凭异常判断事务是否提交。

mermaid
flowchart TD
    A["应用发送COMMIT"] --> B["数据库完成持久化"]
    B --> C["返回成功响应"]
    C --> D["网络在响应到达前断开"]
    D --> E["应用得到连接异常"]
    E --> F["业务结果处于未知状态"]

此时直接重试“创建订单/扣款”可能重复执行。治理:业务唯一请求号、唯一索引、幂等状态机、按业务号查询最终结果和补偿。不要对所有 SQLException 无脑重试;死锁/序列化冲突可在幂等边界内有限退避重试,语法错误和约束错误重试无意义。

二十一、完整商业事务 Demo

java
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、锁、连接池和未知结果问题仍存在。

二十二、连接泄漏是怎样发生的

mermaid
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 连接池耗尽

  1. 查看 active、idle、pending、acquire timeout、max pool;
  2. 判断 active 是否长期等于 max;
  3. 获取连续 jstack,找等待 getConnection 和持有连接线程;
  4. 持有者停在 SQL 则查数据库活动会话/慢日志/锁;
  5. 停在 HTTP/MQ 则说明事务内做慢外部调用;
  6. 检查 leak detection 借出堆栈;
  7. 比较请求 TPS、SQL P99、事务持有时长和数据库容量;
  8. 不要第一反应只增大池。

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无脑重试重复写、持续失败分类、幂等、有限退避
大查询全部装ListOOM和长占连接流式/分页/导出任务

二十五、JDK 与 JDBC 版本边界

能力JDK 7JDK 8后续版本
try-with-resources引入,可用于JDBC支持持续增强
JDBC 4.1JDK 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 响应丢失为什么是未知结果,以及为什么必须业务幂等。