JDBC
Transaction
简单调用
同一个 Connection 复用
把一组 SQL 绑定到同一个 Connection(常见用 ThreadLocal),否则每次从连接池拿不同连接就不成“同一事务”。事务边界与状态机
统一做setAutoCommit(false)→ 执行 →commit()/rollback(),并在finally里收尾。异常与回滚规则
决定哪些异常触发回滚(RuntimeException/SQLException等),并做异常转换/包装。资源释放与连接状态复位
关闭ResultSet/Statement,Connection.close()归还连接池;恢复autoCommit/readOnly/isolation,防止连接池“脏状态”污染下次使用。
嵌套调用
核心: 把多层方法调用变成一套可预测的事务边界规则 ,并实现对应的连接/Savepoint/提交回滚行为。
1) 传播语义(Propagation)的决策
封装层要在“内层方法声明了事务”时决定:
REQUIRED:外层已有事务 → 加入同一事务(同Connection,同一个提交点)REQUIRES_NEW:挂起外层事务 → 新开事务(通常需要另一个 Connection)NESTED:在同一事务里做“子事务” →Savepoint(setSavepoint/rollback(savepoint))其他:
SUPPORTS/NOT_SUPPORTED/MANDATORY/NEVER等(本质是加入/不加入/强制/禁止)
2) 事务上下文栈(Transaction Stack)
实现上通常维护一个“栈/计数器”:
最外层:真正 setAutoCommit(false) 并开始占用 Connection
内层 REQUIRED:只做“参与计数”,不真正 commit
最外层结束:才真正 commit/rollback 并归还连接
3) Savepoint 管理(对应 NESTED)
封装层会:
进入内层:创建
Savepoint内层失败:只回滚到
Savepoint(不影响外层之前的更改)内层成功:释放/忽略
Savepoint(数据库/驱动差异)
注意 :并非所有数据库/驱动/事务管理器都支持或默认启用 NESTED。
4) 挂起/恢复(对应 REQUIRES_NEW)
封装层要做的事更“重”:
把外层绑定的 Connection/事务状态暂存
从池里再拿一个 Connection 开启新事务
新事务结束后:close 归还新连接
再把外层 Connection 恢复绑定继续跑
5) 回滚标记(Rollback-only)
典型坑:内层 REQUIRED 抛异常后被上层 catch 住继续执行。
封装层通常会设置 rollback-only 标志:
内层一旦“需要回滚”,即使外层不再抛异常
最外层
commit()时也会改为rollback(或抛异常提示事务已标记回滚)
DataSource
最核心:Connection 的获取与配置入口
DataSource#getConnection() 做的事情通常不只是 new 一个连接,而是:
选择/创建/复用底层物理连接(
socket到数据库)返回一个 逻辑Connection (很多时候是代理对象 wrapper)
对连接施加初始化配置(部分由连接池或驱动执行):
autoCommit默认值transactionIsolationreadOnlysession变量(例如SET...)网络超时、字符集等(视驱动/DB)
这也解释了:事务框架依赖 DataSource,是为了获取“受治理的连接入口”,而不是裸连。
连接池化:性能与资源上限的封装(最常见场景)
现实里你用到的 DataSource 多半是连接池实现(HikariCP/DBCP/c3p0/Druid 等),它封装了:
连接复用 :避免频繁 TCP + TLS + 认证握手
并发控制 :最大连接数、获取超时、等待队列
连接健康检查::
borrow时校验、idle时校验、定期keepalive“坏连接剔除”与重建
泄漏检测 :连接借出太久报警(leak detection)
连接回收语义:
Connection.close()在池里通常是“归还”,不是物理关闭监控指标:
active/idle/wait、获取耗时、失败率等(不同池暴露方式不同)
所以 DataSource 实际上是你系统的 DB 会话资源调度器。
连接代理:把“治理能力”挂到 Connection 上
连接池往往返回代理 Connection,封装:
拦截
close()→ 归还池拦截
commit/rollback、setAutoCommit→ 记录状态,归还时恢复拦截
createStatement/prepareStatement→ 追踪statement,用于清理/监控驱动级能力增强:超时设置、日志、
Tracing(OpenTelemetry)等
这就是为什么“事务结束要恢复连接属性”这么关键:连接会被复用给下一个请求。
多数据源与路由:选择哪个数据库的封装
很多系统里 DataSource 不是单个库,而是“路由器”:
读写分离:读库/写库路由
分库分表:按
key选择shard多租户:按
tenant选择库主备/故障切换:按健康状态切换
从事务角度看,这会引出一个硬约束:
同一事务内必须固定路由结果(尤其写入场景),否则你会在一个事务里打到多个库 —— 直接破坏语义。
安全与隔离边界:凭证与权限控制
DataSource#getConnection(user, pass) 或容器管理的 DataSource 还会封装:
凭证管理(不在代码里裸写)
权限隔离(不同用户/不同
schema)审计(谁连了、做了什么——更多在
DB或中间件层)
与 Transaction 的边界关系
Transaction 封装的是 Connection 的事务状态与边界纪律
DataSource 封装的是 Connection 的来源与治理(池化/路由/健康/监控/安全
Cursor
对应到 JDBC API,最直接的载体是:
ResultSet:游标式读取接口(next()/previous()/absolute()/relative())Statement/PreparedStatement:产生ResultSet的执行器数据库侧
cursor(实现细节):可能是真·服务端cursor,也可能是一次性把结果全拉到客户端做缓冲(取决于驱动/配置/fetchSize)
游标的两种常见实现形态
1) Client-side cursor(客户端缓存/缓冲)
驱动把结果一次性(或分批)拉到客户端内存里,然后
ResultSet在本地迭代优点:实现简单;支持更多滚动能力(scroll)。
缺点:大结果集会吃内存;网络峰值高;延迟抖动明显。
2) Server-side cursor(服务端游标 / 流式)
数据库在服务端保留 cursor 状态,客户端通过协议“要下一批”。
优点:大结果集更稳(低内存);可以真正流式消费(streaming)。
缺点:服务端要保留资源(内存/锁/临时空间);需要保持连接打开;事务/会话管理更敏感。
JDBC 只给你 ResultSet 这种抽象,是否服务端游标由驱动和数据库决定。 setFetchSize()、 ResultSet 类型、连接属性会影响实现策略
SavePoint
Savepoint (保存点)是事务内部的一个“检查点(checkpoint )”:你可以在同一个事务里打标记,然后在需要时把事务回滚到该标记,而不是把整个事务全部回滚。
在 JDBC 里它对应 java.sql.Savepoint ,由 Connection.setSavepoint() 创建,配合:
conn.rollback(savepoint):回滚到保存点(撤销保存点之后的修改)conn.releaseSavepoint(savepoint):显式释放保存点(可选但推荐)
Savepoint 解决什么问题
1) 部分失败不影响全局事务
你想在一个大事务里做多个步骤:
步骤 A 必须成功
步骤 B 尝试做,失败了就撤销 B,但仍然继续执行 C,并最终提交 A + C
这时就可以在 B 之前创建保存点。
2) 框架层的 NESTED 事务语义
很多事务框架里的 PROPAGATION_NESTED 常用 Savepoint 实现“嵌套事务”:
内层失败 → 回滚到保存点
外层继续 → 外层最终决定
commit/rollback
注意:这不是 REQUIRES_NEW (新连接新事务), Savepoint 仍然在同一个事务里,外层回滚会吞掉一切。
JDBC 使用示例(概念级)
底层语义与限制
1) 仍然是同一事务、同一 Connection
Savepoint不会创建新事务不会释放锁到事务结束(很多数据库锁在
commit/rollback才释放)外层
rollback()会把整个事务清空,不管你savepoint怎么玩
2) 保存点是“栈式”的更自然
你可以创建多个保存点,回滚到较早的保存点会隐含地丢掉其后的保存点(不同数据库细节略有差异,但设计上别指望随意跳来跳去像随机访问)
3) 不是所有数据库/驱动都完全一致
JDBC 定义了 API,但具体行为和性能成本依赖数据库(例如保存点数量、日志开销、和某些 DDL 的交互等)