JDBC = Java 访问数据库的最底层规范:从 DriverManager 到 ORM
哪怕 2025 年了,JDBC 仍然是 Java 后端开发的“必修底层课”:它不时髦,但永远躺在调用栈最下面;它露出很少,但一出问题就是大问题;理解它,不是为了多写几行原生 JDBC,而是:让你在面对 ORM 和连接池时,真正知道自己在干什么。
一、为什么 2025 年了还值得聊 JDBC?
说实话,很多人一听 “JDBC”,心里第一反应是:
啊,不就是
getConnection()+prepareStatement()那点老东西嘛, 现在项目里不都是 Spring Data JPA、MyBatis、MyBatis-Plus 吗?
乍一看没错,但只要你真正在生产项目里干过一段时间,就会发现:所有“高级”的持久层框架,最后都逃不过 JDBC 这道坎。
几个你很可能真实遇到的场景:
-
场景 1:线上连接池打满,服务雪崩
-
监控上看到 HikariCP / Druid 的活跃连接数飙升、等待队列拉满;
-
排查到处都是
getConnection(),却说不清:连接池到底怎么跟 JDBC 打交道?连接泄漏是卡在哪一层?
-
-
场景 2:ORM 生成的 SQL 跑得飞慢
-
你看到日志里 Hibernate/MyBatis 打出的 SQL;
-
打开监控一看:某条查询耗时 2 秒;
-
你很难判断:
慢在数据库?慢在 JDBC?慢在 ORM 的对象映射和缓存?
-
-
场景 3:面试被问懵
-
“JDBC 和 ORM 的关系是什么?”
-
“
DriverManager具体做了什么?Driver 是怎么被发现和加载的?” -
“连接池和 JDBC 之间是谁在管事务?”
-
这些问题,框架层都不会替你回答。 回答这些问题的“那一层”,就叫做:JDBC。
更关键的是:
-
你现在写的 MyBatis、JPA、Spring Data JPA、Spring Data JDBC…… 统统都是 站在 JDBC 的肩膀上;
-
如果你只会“调用 Repository / Mapper”,但脑子里没有一张清晰的:
“应用 → ORM / MyBatis → JDBC → 驱动 → 数据库” 的调用图 那么一旦出了复杂问题,你的排查维度永远停留在“猜”。
所以,哪怕 2025 年了,JDBC 仍然是 Java 后端开发的“必修底层课”:
-
它不时髦,但永远躺在调用栈最下面;
-
它露出很少,但一出问题就是大问题;
-
理解它,不是为了多写几行原生 JDBC,而是:
让你在面对 ORM 和连接池时,真正知道自己在干什么。
二、JDBC 到底是什么?规范、驱动还是某个 jar 包?
很多同学对 JDBC 的印象是这样的:
-
“我在项目里加了
mysql-connector-j这个 jar, 然后调用DriverManager.getConnection, 能连上 MySQL,这就是 JDBC 吧?”
这其实只说对了一半。
为了不绕,我们先把 几个容易混淆的概念拆开:
1. JDBC 不是“某一个驱动 jar”
JDBC 本身不等于:
-
不是
mysql-connector-j-x.x.x.jar; -
不是
postgresql-x.x.x.jar; -
更不是某家厂商的私有库。
这些 jar 是什么? 它们是:各个数据库厂商对 JDBC 规范的“实现”——也就是 JDBC 驱动(Driver)。
2. JDBC 首先是一套“规范 + 接口”
在 JDK 里,你会看到这些熟悉的包:
java.sql.*
javax.sql.* (在 Jakarta 之后是 jakarta.sql.*)
里面定义了一堆接口和类,比如:
-
java.sql.Driver -
java.sql.DriverManager -
java.sql.Connection -
java.sql.Statement/PreparedStatement -
java.sql.ResultSet -
java.sql.SQLException -
……
它们一起构成了:
JDBC 规范:Java 世界访问关系型数据库的统一 API 和行为约定。
也就是说:
-
对上:你写业务代码,只需要面向这些标准接口编程;
-
对下:各家数据库厂商通过 实现这些接口 来接入 Java 生态。
3. 驱动(Driver)= JDBC 规范的厂商实现
比如 MySQL 驱动里就会有类似(类名略有不同,但本质一致):
public class com.mysql.cj.jdbc.Driver implements java.sql.Driver {
// 实现 connect、acceptsURL 等方法
}
PostgreSQL 也会有:
public class org.postgresql.Driver implements java.sql.Driver {
// 实现自己的连接逻辑
}
它们要做的事情可以用一句话概括:
“用 JDBC 规定好的接口形式,把 Java 世界的调用,翻译成各自数据库协议的一串字节,然后通过 Socket 发出去,接收结果,再翻译回来。”
所以整体关系可以画成这样:
你的业务代码
↓ (面向 JDBC API 编程)
java.sql.*(JDBC 规范 / 接口)
↓ (由驱动实现)
MySQL / PG / Oracle ... JDBC Driver
↓
具体数据库
JDBC 本质上更像是《接口说明书 + 一套 Java API》 真正干活的是各家的 JDBC Driver 实现。
三、从一行 DriverManager.getConnection() 开始拆:DriverManager & Driver
聊 JDBC,如果只停留在「会写 CRUD」其实没什么感觉。 真正有意思的,是把这句经典代码拆开来看:
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=UTC",
"root",
"password"
);
看起来只是:
“传一个 URL 和用户名密码,给我一个 Connection。”
实际上,这一行背后,至少发生了这么几件事:
-
JVM 里已经有一堆 Driver 注册进来了(MySQL、PG、Oracle…)
-
DriverManager会在这堆 Driver 里“挑一个合适的”; -
找到之后,会调用这个 Driver 的
connect()方法; -
Driver 再真正去:
-
解析 URL;
-
建立 Socket;
-
协议握手、认证;
-
创建一个实现了
java.sql.Connection的对象返回给你。
-
我们把这条链路按角色讲清楚。
3.1 DriverManager:JDBC 世界的“路由器”
DriverManager 不是驱动本身,它更像一个 “驱动管理中心 / 路由器”,核心职责有两件:
-
维护驱动列表
-
内部有一个
registeredDrivers的集合; -
存放所有已经注册的
java.sql.Driver实例。
-
-
按 URL 选择合适的驱动并建立连接
-
你调用
getConnection(url, info)时:-
遍历所有已注册的 Driver;
-
依次问它们:“你认这个 URL 吗?”;
-
找到第一个说“我认”的那个,让它去
connect()。
-
-
用一个简化版伪代码来感受一下 getConnection(url, info) 的流程(只保留核心逻辑):
public static Connection getConnection(String url, Properties info) throws SQLException {
SQLException reason = null;
// 遍历所有已注册的 Driver
for (DriverInfo di : registeredDrivers) {
Driver driver = di.driver;
// 先问:你能不能处理这个 URL?
if (driver.acceptsURL(url)) {
try {
// 能处理就尝试建立连接
Connection conn = driver.connect(url, info);
if (conn != null) {
return conn; // 成功则直接返回
}
} catch (SQLException ex) {
reason = ex; // 记下异常,最后统一抛
}
}
}
// 没有任何 Driver 能处理这个 URL
if (reason != null) throw reason;
throw new SQLException("No suitable driver found for " + url);
}
注意几点:
-
DriverManager不会自己去连数据库,它只是“调度者 + 分发器”; -
真正连接数据库的是每个具体 Driver 的
connect()实现。
3.2 Driver:谁来负责“我认这个 URL,我去连”
java.sql.Driver 接口是 JDBC 规范要求各家驱动必须实现的核心接口。
它的职责本质上只有两件事:
-
URL 过滤:我能不能处理这个 URL?
boolean acceptsURL(String url) throws SQLException;比如 MySQL 驱动内部会判断:
-
URL 前缀是不是
jdbc:mysql:; -
格式对不对,参数是否满足基本要求等。
-
-
建立连接:根据 URL 和参数创建 Connection
Connection connect(String url, Properties info) throws SQLException;这里才是重头戏,一般会做:
-
解析 URL(主机、端口、数据库名、连接参数等);
-
建立底层 Socket 连接;
-
按照各自的协议(MySQL 协议 / PG 协议……)完成握手和认证;
-
创建一个自己实现的
Connection实例返回。
-
同样,用一个简化伪代码感受一下:
@Override
public Connection connect(String url, Properties info) throws SQLException {
// 1. 不认这个 URL,直接返回 null,让 DriverManager 去找下一个
if (!acceptsURL(url)) {
return null;
}
// 2. 解析 URL
ParsedUrl parsed = parseUrl(url, info);
// 3. 建立底层网络连接、完成认证
Socket socket = openSocket(parsed.host, parsed.port);
handshake(socket, info);
// 4. 返回自己实现的 Connection
return new MySqlConnectionImpl(socket, parsed, info);
}
你在业务代码里看到的:
Connection conn = DriverManager.getConnection(...);
拿到的是一个 java.sql.Connection 接口类型, 但实际上背后是类似 MySqlConnectionImpl 这种“驱动私有实现类”。
3.3 Driver 是怎么“注册”到 DriverManager 里的?
这一步很多人比较模糊,尤其是现在基本不用写 Class.forName 之后。
其实主要有两种机制:
① 早期写法:Class.forName() + 静态代码块注册
在旧时代(以及兼容一些老驱动时),我们会手写:
Class.forName("com.mysql.cj.jdbc.Driver");
这行代码的本质不是“new 一个 Driver”,而是:
-
让 JVM 去加载
com.mysql.cj.jdbc.Driver这个类; -
执行它的 静态代码块。
很多驱动内部会这么写(伪代码):
public class Driver implements java.sql.Driver {
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException e) {
throw new RuntimeException("Can't register driver!", e);
}
}
// 省略 connect / acceptsURL 实现...
}
也就是说:
你一
Class.forName(...), JVM 加载这个类 → 执行 static 块 → 调DriverManager.registerDriver(...)→ 这个 Driver 就出现在registeredDrivers列表里了。
② JDBC 4.0 之后:Service Provider(自动发现)
在 JDBC 4.0 之后,更现代的驱动(比如新版 MySQL/PostgreSQL) 一般不需要你手写 Class.forName 了,它们通过 SPI + ServiceLoader 自动注册。
驱动 jar 里有一个文件:
META-INF/services/java.sql.Driver
内容类似:
com.mysql.cj.jdbc.Driver
org.postgresql.Driver
...
JDK 在初始化 DriverManager 时,会通过 ServiceLoader.load(Driver.class) 扫描这些声明过的实现类,自动实例化并注册。
这就是为什么现在你只要:
-
在依赖里加上 MySQL 驱动;
-
写个正确的
jdbc:mysql://...URL;
不用写任何 Class.forName,DriverManager.getConnection 就能工作。
小结一下这一节的链路:
你的代码:
DriverManager.getConnection(url, user, pass)
↓
DriverManager:
遍历已注册的 Driver 列表
→ 调 driver.acceptsURL(url)
↓
匹配上的 Driver:
调 connect(url, props)
→ 解析 URL、建连接、握手认证
→ new 一个具体 Connection 实现类
↓
返回给你:
一个实现了 java.sql.Connection 的对象(接口视角)
从这一行 getConnection() 往下追, 你就会发现 JDBC 世界里几个最核心的角色:
-
DriverManager:驱动的“管理者”和“选择器”; -
Driver:具体数据库的“接入实现”和“Connection 工厂”; -
Connection:后面所有 SQL、事务、Statement、ResultSet 的起点。
四、Connection / Statement / ResultSet:JDBC 三件套的角色划分
前面我们说过:DriverManager.getConnection(...) 最终给你的,是一个实现了 java.sql.Connection 接口的对象。 从这一刻起,后面所有的数据库操作,基本都绕不开三件套:
Connection → Statement / PreparedStatement → ResultSet
很多初学者只知道“照着 Demo 写”,但脑子里没有清晰分工。 我们不妨先把它们当成一个小团队来看:
-
Connection:组长(会话 & 事务管理者)
-
Statement / PreparedStatement:执行 SQL 的“送信员”
-
ResultSet:数据库结果的“游标视图”
4.1 Connection:一次“会话” + 事务的承载者
Connection 的角色可以用一句话概括:
它代表了你和数据库之间的一次“会话”, 承载着整个会话期的配置和事务边界。
它负责的事情主要有:
-
管理事务
conn.setAutoCommit(false); // 关闭自动提交,开始手动事务 // ... 多条 SQL ... conn.commit(); // 提交事务 conn.rollback(); // 回滚事务-
默认情况下,
autoCommit = true,每条 DML(INSERT/UPDATE/DELETE)都是一个独立事务; -
当关闭自动提交后,多条语句就被“绑”在一个事务里,成功一起成功,失败一起失败。
-
-
保存会话级配置
比如:
conn.setReadOnly(true); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);-
隔离级别、只读模式、当前 schema/catalog 等, 其实都是挂在
Connection上的“会话状态”。
-
-
创建 Statement / PreparedStatement
-
执行 SQL 并不是
Connection自己干的; -
它只负责创建执行 SQL 的“工具人”:
Statement stmt = conn.createStatement(); PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");你可以把 Connection 理解成: “打开数据库的一扇门,这扇门背后站着一堆小弟(Statement)等你指挥。”
-
4.2 Statement / PreparedStatement:SQL 语句的“载体”
Statement 系列的职责只有一件事:
把你写的 SQL 发给数据库执行,并接收执行结果。
典型有三种常见用法:
-
执行无参数 SQL:
StatementString sql = "CREATE TABLE users (id BIGINT PRIMARY KEY, name VARCHAR(50))"; try (Statement stmt = conn.createStatement()) { stmt.executeUpdate(sql); }-
通常用于 DDL(建表、加索引)或简单、不带参数的语句;
-
缺点是不能安全地拼接外部输入,否则容易 SQL 注入。
-
-
执行带参数 SQL:
PreparedStatementString sql = "SELECT id, name, email FROM users WHERE id = ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setLong(1, userId); // 第 1 个 ? 绑定 userId try (ResultSet rs = ps.executeQuery()) { // 处理结果 } }相比
Statement的优势:-
使用
?占位符 +setXxx()绑定参数,天然防止 SQL 注入; -
在很多数据库中,预编译语句有缓存,重复执行时性能更好。
-
-
根据语句类型选择
executeUpdate/executeQuery/execute-
executeUpdate():用于 DDL 和 DML 中的 INSERT/UPDATE/DELETE,返回影响行数; -
executeQuery():用于 SELECT,返回ResultSet; -
execute():通用型,返回布尔值表示有没有结果集(一般框架内部才用)。
-
可以理解为:
Statement / PreparedStatement 就是 JDBC 世界里 “把 SQL 丢给数据库、拿回结果” 的那双手。
4.3 ResultSet:结果集的“游标式快照”
当你执行查询语句时:
ResultSet rs = ps.executeQuery();
拿到的 ResultSet 不是一个“List”,而是一个游标:
-
一开始“指针”在第一行之前;
-
每次
next(),指针下移一行; -
通过
getXxx("columnName")拿当前行的列值。
典型用法:
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
long id = rs.getLong("id");
String name = rs.getString("name");
String email = rs.getString("email");
// 组装成对象或直接使用
}
}
几个关键点:
-
ResultSet 跟 Connection 生命周期强相关
-
ResultSet依赖Statement和Connection; -
一般关闭顺序:
ResultSet→Statement→Connection; -
try-with-resources 可以自动帮你管理。
-
-
滚动/只进等高级用法
-
默认情况下,是“只进、向前”的结果集;
-
也可以创建可滚动、可更新的 ResultSet(用得少,这里不展开)。
-
你可以把 ResultSet 理解成:
“数据库在某个时间点返回的结果快照 + 一个可以在这份快照上来回移动的指针”。
4.4 小结:三件套的分工
用一句话总结三件套:
Connection:负责“开会”和“会后总结”(会话 + 事务 + 配置)
Statement/PreparedStatement:负责“把要说的话讲出去”(执行 SQL)
ResultSet:负责“把别人回复的内容记下来翻阅”(读取查询结果)
而你平时写的那些 ORM / MyBatis,本质上就是在背后帮你:
-
自动创建和管理 Connection;
-
自动构造各种 Statement / PreparedStatement;
-
自动把 ResultSet 里的每一列映射到你的实体对象字段上。
五、DDL/DML 在 JDBC 中是怎么跑的?(最小 CRUD Demo)
理解了三件套,我们来看最常见的两类 SQL:
-
DDL(Data Definition Language):数据定义语言 → 改“结构”
-
CREATE TABLE/ALTER TABLE/DROP TABLE…
-
-
DML(Data Manipulation Language):数据操作语言 → 改“数据”
-
INSERT/UPDATE/DELETE/SELECT
-
5.1 DDL:建一个 users 表
我们先用一段最简单的 JDBC 代码,创建一张 users 表:
String url = "jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=UTC";
String user = "root";
String password = "password";
String ddl = """
CREATE TABLE IF NOT EXISTS users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""";
try (Connection conn = DriverManager.getConnection(url, user, password);
Statement stmt = conn.createStatement()) {
int result = stmt.executeUpdate(ddl);
System.out.println("DDL 执行完成,返回值 = " + result); // 通常对 DDL 没什么意义
} catch (SQLException e) {
e.printStackTrace();
}
要点:
-
DDL 通常使用
Statement即可(SQL 一般比较固定); -
通过
executeUpdate执行即可,返回值一般为 0,重点在“有没有异常”。
5.2 DML:用 PreparedStatement 写一个最小 CRUD
假设我们的 users 表已经建好,接下来实现一个最小的:
-
插入用户(INSERT)
-
更新邮箱(UPDATE)
-
按 id 查询(SELECT)
-
删除用户(DELETE)
public class JdbcCrudDemo {
private static final String URL = "jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=UTC";
private static final String USER = "root";
private static final String PASSWORD = "password";
public static void main(String[] args) {
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) {
conn.setAutoCommit(false); // 手动事务,后面一节会展开讲
try {
long userId = insertUser(conn, "Alice", "alice@example.com");
System.out.println("插入用户成功,id = " + userId);
updateUserEmail(conn, userId, "alice.new@example.com");
System.out.println("更新邮箱成功");
queryUser(conn, userId);
deleteUser(conn, userId);
System.out.println("删除用户成功");
conn.commit(); // 所有操作成功,提交事务
} catch (Exception e) {
conn.rollback(); // 出现任何异常,回滚
throw e;
}
} catch (SQLException e) {
e.printStackTrace();
}
}
// INSERT:插入一个用户,返回自增主键
private static long insertUser(Connection conn, String name, String email) throws SQLException {
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, name);
ps.setString(2, email);
int affected = ps.executeUpdate();
if (affected != 1) {
throw new SQLException("插入用户失败,受影响行数 = " + affected);
}
// 取回数据库生成的自增主键
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) {
return rs.getLong(1);
} else {
throw new SQLException("插入成功但未获取到自增主键");
}
}
}
}
// UPDATE:更新用户邮箱
private static void updateUserEmail(Connection conn, long userId, String newEmail) throws SQLException {
String sql = "UPDATE users SET email = ? WHERE id = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, newEmail);
ps.setLong(2, userId);
int affected = ps.executeUpdate();
if (affected != 1) {
throw new SQLException("更新邮箱失败,受影响行数 = " + affected);
}
}
}
// SELECT:按 id 查询用户
private static void queryUser(Connection conn, long userId) throws SQLException {
String sql = "SELECT id, name, email, created_at FROM users WHERE id = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, userId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
long id = rs.getLong("id");
String name = rs.getString("name");
String email = rs.getString("email");
Timestamp createdAt = rs.getTimestamp("created_at");
System.out.printf("查询到用户:id=%d, name=%s, email=%s, created_at=%s%n",
id, name, email, createdAt);
} else {
System.out.println("未找到 id=" + userId + " 的用户");
}
}
}
}
// DELETE:删除用户
private static void deleteUser(Connection conn, long userId) throws SQLException {
String sql = "DELETE FROM users WHERE id = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, userId);
int affected = ps.executeUpdate();
if (affected != 1) {
throw new SQLException("删除用户失败,受影响行数 = " + affected);
}
}
}
}
通过这个 Demo,你可以清楚看到:
-
DDL:
Statement + executeUpdate; -
DML(INSERT/UPDATE/DELETE):
PreparedStatement + executeUpdate; -
DML(SELECT):
PreparedStatement + executeQuery + ResultSet;
在“裸 JDBC 时代”,你要为每张表、每个 SQL 反复写这些模板代码; ORM / MyBatis 做的事情,就包含了把这些重复劳动统一收走。
六、事务与自动提交:setAutoCommit(false) 背后发生了什么?
事务是 JDBC 里一个经常“被忽视,但一旦犯错就是大事故”的点。 我们从一个最常见的问题说起:
“为什么我的插入/更新明明没报错,但数据库里就是没看到数据?”
十有八九,是对 auto-commit 和手动事务 的理解不清。
6.1 autoCommit = true:每条语句都是一个“隐式事务”
JDBC 规范规定:
-
默认情况下,一个新的
Connection,autoCommit = true; -
这意味着:
每一条 DML 语句(INSERT/UPDATE/DELETE)都被当作一个独立事务, 执行成功后会自动
commit。
简单理解就是:
try (Connection conn = getConnection()) {
// autoCommit 默认是 true
insertA(conn); // 独立事务 1
updateB(conn); // 独立事务 2
deleteC(conn); // 独立事务 3
}
只要每条语句本身没抛异常,结果就立刻提交到数据库了。 你不会有“回滚整批操作”的机会。
这在简单场景下没问题,但在“要么都成功,要么都失败”的业务里就有风险了,例如:
-
扣库存 + 记录流水
-
下订单 + 写日志 + 更新账户余额
如果中途有一条 SQL 失败,你很可能得到“扣了库存但没写日志”“扣了钱但没下单”这种不一致状态。
6.2 关闭自动提交:一次真正意义上的“事务”
为了保证一组操作的原子性,我们一般会这么写:
try (Connection conn = getConnection()) {
conn.setAutoCommit(false); // ① 关闭自动提交,开启手动事务
try {
// ② 一组要么都成功,要么都失败的操作
insertA(conn);
updateB(conn);
deleteC(conn);
conn.commit(); // ③ 全部成功,提交事务
} catch (Exception e) {
conn.rollback(); // ④ 任意一步失败,回滚事务
throw e;
}
}
这里有几个关键点:
-
setAutoCommit(false)通常等价于“告诉数据库:我要开始一个显式事务了”; -
在
commit()之前,多个 DML 语句的效果都只对当前事务可见; -
调用
rollback()会让这些尚未提交的改动全部撤销; -
事务结束后(无论
commit还是rollback),有的数据库会把连接恢复为自动提交,这取决于驱动 & 数据库实现, 所以很多框架会在连接池归还连接前显式恢复 autoCommit 状态,避免影响下一个租用者。
结合前面的 CRUD Demo,你再看这段逻辑就很清晰了: 那段代码里把“插入 → 更新 → 查询 → 删除”放在一个手动事务里, 要么四个操作都完成,要么一个都不算数。
6.3 事务隔离级别:JDBC 只是“翻译官”
除了“有事务 / 没事务”,还有一个经常被提到的概念:隔离级别。
在 JDBC 里,你可以通过 Connection 设置隔离级别:
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
常见的几种:
-
TRANSACTION_READ_UNCOMMITTED -
TRANSACTION_READ_COMMITTED -
TRANSACTION_REPEATABLE_READ -
TRANSACTION_SERIALIZABLE
JDBC 做的事情其实很简单:
它只是提供了一组常量 + 一个方法, 把你在 Java 代码里的设置,翻译成各自数据库的命令。
真正决定“会不会脏读 / 不可重复读 / 幻读”的,是数据库本身:
-
MySQL InnoDB 在
REPEATABLE_READ下如何处理; -
PostgreSQL 在
READ_COMMITTED下是什么行为;
这些都不是 JDBC 定义的,而是各家数据库的实现。
JDBC 只负责这件事:
你说:我要 READ_COMMITTED
JDBC:好的,我翻译成一条数据库能听懂的命令发过去
数据库:收到,我按自己的规则执行
6.4 为什么事务这层理解清楚很重要?
因为一旦你上了:
-
Spring 事务(
@Transactional) -
连接池(HikariCP / Druid)
-
ORM / MyBatis
你会遇到各种迷惑行为:
-
明明加了
@Transactional,为什么好像没回滚? -
为什么一个服务里某个方法改了
autoCommit,会影响到完全不相干的地方? -
连接池里的“归还连接”到底会不会
rollback/ 重置事务状态?
要搞清楚这些问题,绕不开 JDBC 这一层:
-
Connection是谁创建的?谁在池子里维护? -
setAutoCommit(false)是在什么时机被调用的? -
commit()/rollback()是谁来触发的?
理解 JDBC 层的事务行为之后,你再看 Spring 事务传播机制、再看 ORM 的 Session / EntityManager,就不会只停留在“背注解”的层面了。
七、连接池与 JDBC:为什么不能每次都 new 一个 Connection?
如果你只在本地写过 Demo,很容易有一种错觉:
try (Connection conn = DriverManager.getConnection(...)) {
// do something
}
跑得也挺快啊?为啥生产上大家都要搞什么 HikariCP、Druid、C3P0?
关键在于:**getConnection() 背后可不是“new 一个 Java 对象”这么简单。**
7.1 一次真正的数据库连接成本有多高?
当你调用:
DriverManager.getConnection(url, user, password)
驱动实际要做的事情大致包括:
-
解析 URL:主机、端口、数据库名、连接参数;
-
建立网络连接:TCP 三次握手;
-
协议握手:MySQL/PG/Oracle 各自的一套 binary 协议交互;
-
认证:用户名/密码、权限检查、可能还有 SSL、加密协商;
-
初始化会话状态:字符集、时区、事务隔离级别、某些 session 变量等。
这些都是真金白银的网络 IO + CPU 开销。
本地 Demo 只有你一个人在玩,感觉不明显; 一旦到了线上高并发场景:
-
每个请求都
new Connection; -
每条 SQL 都走一遍 TCP + 握手 + 认证;
很快你就会遇到:
-
数据库连接数飙升到上限;
-
数据库 CPU 飙高;
-
应用端请求大量阻塞在“获取连接”这一步。
7.2 连接池解决的核心问题
连接池(Connection Pool)背后的核心思想很简单:
把“昂贵的连接创建”摊薄掉,把“有限的数据库连接资源”管理起来。
具体来说,它做了几件事:
-
预先创建一批连接
-
比如启动时先创建 10 个连接;
-
后续根据负载动态扩容到最多 50 个。
-
-
复用连接
-
请求 A:向池子借一个 Connection,用完
close()→ 实际是“归还到池子”; -
请求 B:再来借 → 得到的是同一个底层物理连接。
-
-
限制最大并发连接数
-
池子里最多就 50 个连接;
-
超过的请求要么阻塞等待,要么直接抛异常;
-
起到了“防止把数据库撑爆”的阀门作用。
-
-
做健康检查和泄漏检测
-
空闲太久的连接会被回收;
-
检测“借出去很久没还”的连接,帮助排查连接泄漏问题;
-
对“坏掉的连接”(网络中断、数据库重启)做自动剔除和重建。
-
7.3 在 JDBC 视角下,连接池长什么样?
从代码层面看,连接池基本就是:实现了 javax.sql.DataSource 的一个类。
比如在 HikariCP 里,你会这样用:
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://localhost:3306/demo_db");
ds.setUsername("root");
ds.setPassword("password");
ds.setMaximumPoolSize(20);
try (Connection conn = ds.getConnection()) {
// do something...
}
注意两点:
-
你拿到的还是一个
java.sql.Connection对象; -
但这个 Connection 的
close()被池子“代理”了:-
看起来你关掉了连接;
-
实际上只是把连接“放回池子”,以便下一个请求复用。
-
从调用链上看,变成了:
你的代码:
DataSource.getConnection()
↓
连接池:
从池子里借出一个空闲的 Connection(或者新建一个)
↓
底层:
真正的 JDBC Connection(由 DriverManager/Driver 创建)
所以你可以这么记:
DriverManager + Driver 负责“怎么连上数据库”; 连接池负责“连上之后,这些连接怎么高效、安全地被复用”。
八、JDBC vs ORM / MyBatis:为什么上层还要套那么多框架?
这时候你可能会问:
我已经知道怎么用 JDBC 操作数据库了, 为啥项目里大家还要用 Hibernate / JPA / MyBatis / MyBatis-Plus 这一堆东西?
简单对比一下,同样一个“按 id 查询 User”的逻辑。
8.1 纯 JDBC 写法:一切从零开始
public User findById(Connection conn, long id) throws SQLException {
String sql = "SELECT id, name, email, created_at FROM users WHERE id = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) {
return null;
}
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
user.setCreatedAt(rs.getTimestamp("created_at").toInstant());
return user;
}
}
}
你自己处理了:
-
SQL 字符串;
-
参数绑定;
-
ResultSet解析; -
字段 → 对象属性映射。
这在一个小 Demo 里没问题; 但换成一个有几十张表、几百个 CRUD 场景的项目,你会发现:
-
大量重复代码;
-
一堆 copy-paste;
-
一改字段就要到处找 SQL、改映射。
8.2 ORM / SQL Mapper 做的事
① ORM(比如 JPA / Hibernate)
你只需要定义实体类:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
private String email;
// getter / setter ...
}
然后就可以:
User user = entityManager.find(User.class, 1L);
ORM 会帮你:
-
生成合适的 SQL;
-
用 JDBC 执行;
-
把
ResultSet映射回User对象; -
管理实体的生命周期(一级缓存、脏检查、懒加载等)。
② SQL Mapper(比如 MyBatis)
你定义 Mapper 接口 + XML/注解 SQL:
public interface UserMapper {
User selectById(@Param("id") Long id);
}
<select id="selectById" parameterType="long" resultType="User">
SELECT id, name, email, created_at
FROM users
WHERE id = #{id}
</select>
然后:
User user = userMapper.selectById(1L);
MyBatis 会帮你:
-
处理参数绑定;
-
执行 SQL;
-
把
ResultSet按映射规则转换成User对象。
可以看到:
ORM / MyBatis 的核心价值就是: 把“操作数据库”从“面向 SQL 字符串 + ResultSet” 提升到“面向对象 / 接口”的抽象层。
8.3 那 JDBC 和它们的关系是什么?
一句话:
JDBC 是地基,ORM / MyBatis 是盖在地基上的楼。
-
ORM / MyBatis 内部还是用 JDBC:
-
拿连接(通常是从连接池 DataSource 里拿);
-
创建
PreparedStatement; -
执行 SQL;
-
解析
ResultSet;
-
-
只是这些细节被封装掉了,你在业务层更关注:
-
对象;
-
仓储接口(Repository/Mapper);
-
事务边界。
-
这也是为什么:
-
当 ORM 生成的 SQL 很慢时,你依然要看 JDBC 层的执行日志;
-
当连接池出现问题时,你需要知道“ORM 是如何获取和释放 Connection 的”。
理解 JDBC,是理解这些高层框架“底座行为”的前提。
九、JDBC 实战中的常见坑 + 最佳实践
讲了这么多原理,落地到实际项目中,JDBC 有几类经典“坑点”,非常值得单独列一节当作 checklist。
9.1 忘记关闭资源 / 关闭顺序不当
经典写法(反例):
Connection conn = DriverManager.getConnection(...);
PreparedStatement ps = conn.prepareStatement(...);
ResultSet rs = ps.executeQuery();
// 忘记 close,或者只关了 rs 没关 ps/conn
后果:
-
连接池里的连接数越用越多;
-
最终“看起来没多少请求”,但数据库连接数却打满。
最佳实践:
-
一律使用 try-with-resources:
try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { // ... } } -
连接池场景下,
close()不是真的断开连接,而是“归还到池子”,不用心疼。
9.2 字符串拼 SQL,导致 SQL 注入
反例:
String sql = "SELECT * FROM users WHERE name = '" + name + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
如果 name 来自用户输入,你可能被直接注入成:
SELECT * FROM users WHERE name = '' OR 1=1 --'
最佳实践:永远使用 PreparedStatement + ? 占位符。
String sql = "SELECT * FROM users WHERE name = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, name);
try (ResultSet rs = ps.executeQuery()) {
// ...
}
}
9.3 不合理的事务边界
反例 1:该用事务的地方没用事务
// 默认 autoCommit = true
insertOrder(conn);
deductStock(conn);
insertLog(conn);
// 中间任何一步失败,之前的操作都无法回滚
反例 2:事务范围过大、长事务占用连接
conn.setAutoCommit(false);
// 这里做了一堆远程调用、文件 IO、复杂计算
// 很久之后才 commit()
// 这一整个时间段里,数据库连接被占着不放
最佳实践:
-
有明确原子性要求的一组操作,要显式事务:
-
setAutoCommit(false)→ try … commit → catch … rollback;
-
-
控制事务粒度:
-
事务中避免长时间阻塞(远程调用/复杂计算);
-
保证“打开事务 → 执行 SQL → 立刻提交/回滚”的节奏。
-
9.4 没有设置超时导致请求挂死
很多代码默认不设置任何超时:
-
SQL 卡在锁等待 / 慢查询;
-
线程一直等,连接一直占着;
-
连接池很快被占满,引发级联雪崩。
最佳实践:
-
JDBC 层合理设置查询超时:
ps.setQueryTimeout(30); // 秒 -
数据库层设置语句超时 / 锁等待超时;
-
业务层(比如 Spring)设置事务超时。
9.5 批量操作没用 batch,导致性能惨烈
反例:循环里一条条 insert:
for (User u : users) {
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, u.getName());
ps.executeUpdate();
}
}
最佳实践:使用 batch:
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
int batchSize = 0;
for (User u : users) {
ps.setString(1, u.getName());
ps.setString(2, u.getEmail());
ps.addBatch();
if (++batchSize % 1000 == 0) {
ps.executeBatch(); // 每 1000 条 flush 一次
batchSize = 0;
}
}
if (batchSize > 0) {
ps.executeBatch();
}
}
十、总结:一张图串起 JDBC、连接池、ORM、Spring
最后,用一张逻辑图把本文所有角色串起来:
(Controller / Service)
你的业务代码
│
▼
(Repository / Mapper / DAO 层)
┌─────────────────────────────────┐
│ ORM / MyBatis / JPA │
│ - 实体映射 / SQL 生成或管理 │
│ - 一级缓存 / 懒加载 / 级联等 │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ JDBC API │
│ Connection / Statement / │
│ PreparedStatement / ResultSet │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 连接池 DataSource │
│ - 连接复用 / 最大连接数控制 │
│ - 泄露检测 / 健康检查 │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ JDBC Driver (厂商实现) │
│ - DriverManager 注册 / 发现 │
│ - connect() / 协议握手 / 认证 │
└─────────────────────────────────┘
│
▼
具体数据库
(MySQL / PostgreSQL / Oracle ...)
再加上 Spring 事务 放的位置:
-
Spring 通过
@Transactional拦截你的 Service 方法; -
在方法开始之前:
-
从连接池拿一个 Connection;
-
设置
autoCommit = false;
-
-
在方法正常返回时:
-
调用
commit();
-
-
在抛异常时:
-
调用
rollback();
-
-
最后把 Connection 交还给连接池。
所以,Spring 事务、本地 JDBC 事务、本地 Connection, 本质上都在操作同一层:JDBC Connection 的事务状态。
把这几层关系想清楚之后,JDBC 就不再只是“复制粘贴 Demo 里的三行代码”,而是:
-
一个清晰可见的“最底层规范”;
-
一个你在排查性能问题、连接池问题、事务问题时,必须理解的“地基”。
以后再看到:
-
DriverManager.getConnection(...); -
HikariCP 里那一排连接池配置;
-
Hibernate/MyBatis 打出来的 SQL 日志;
你脑子里就不只是“能跑就行”,而是:
“哦,这是在 JDBC 这一层怎么走的; 再往下是驱动;再往上是 ORM; 事务和连接池又是怎么跨这几层配合的。”
更多推荐


所有评论(0)