Mind and Hand Help

JDBC

Transaction

简单调用

  1. 同一个 Connection 复用
    把一组 SQL 绑定到同一个 Connection(常见用 ThreadLocal),否则每次从连接池拿不同连接就不成“同一事务”。

  2. 事务边界与状态机
    统一做 setAutoCommit(false) → 执行 → commit()/rollback() ,并在 finally 里收尾。

  3. 异常与回滚规则
    决定哪些异常触发回滚(RuntimeException/SQLException 等),并做异常转换/包装。

  4. 资源释放与连接状态复位
    关闭 ResultSet/StatementConnection.close() 归还连接池;恢复 autoCommit/readOnly/isolation ,防止连接池“脏状态”污染下次使用。

嵌套调用

核心把多层方法调用变成一套可预测的事务边界规则 ,并实现对应的连接/Savepoint/提交回滚行为。

1) 传播语义(Propagation)的决策

封装层要在“内层方法声明了事务”时决定:

  • REQUIRED :外层已有事务 → 加入同一事务(同 Connection ,同一个提交点)

  • REQUIRES_NEW :挂起外层事务 → 新开事务(通常需要另一个 Connection)

  • NESTED :在同一事务里做“子事务” → SavepointsetSavepoint/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 默认值

    • transactionIsolation

    • readOnly

    • session 变量(例如 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/rollbacksetAutoCommit → 记录状态,归还时恢复

  • 拦截 createStatement/prepareStatement → 追踪 statement ,用于清理/监控

  • 驱动级能力增强:超时设置、日志、 TracingOpenTelemetry )等

这就是为什么“事务结束要恢复连接属性”这么关键:连接会被复用给下一个请求。

多数据源与路由:选择哪个数据库的封装

很多系统里 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 使用示例(概念级)

// @formatter:off conn.setAutoCommit(false); try { // A doA(conn); Savepoint sp = conn.setSavepoint("afterA"); try { // B doB(conn); } catch (SQLException bEx) { // 只撤销 B conn.rollback(sp); // 可选:释放保存点,避免堆积 conn.releaseSavepoint(sp); } // C doC(conn); conn.commit(); } catch (SQLException ex) { conn.rollback(); // 全部撤销 throw ex; } finally { conn.setAutoCommit(true); conn.close(); } // @formatter:on

底层语义与限制

1) 仍然是同一事务、同一 Connection

  • Savepoint 不会创建新事务

  • 不会释放锁到事务结束(很多数据库锁在 commit/rollback 才释放)

  • 外层 rollback() 会把整个事务清空,不管你 savepoint 怎么玩

2) 保存点是“栈式”的更自然

你可以创建多个保存点,回滚到较早的保存点会隐含地丢掉其后的保存点(不同数据库细节略有差异,但设计上别指望随意跳来跳去像随机访问)

3) 不是所有数据库/驱动都完全一致

JDBC 定义了 API,但具体行为和性能成本依赖数据库(例如保存点数量、日志开销、和某些 DDL 的交互等)

27 January 2026