第6篇-连接的生命周期与事务边界
约 1888 字大约 6 分钟
2026-05-24
四、Spring 事务与连接池的协作:"隐形的手"
你可能想知道:Spring 的 @Transactional 注解,是如何与 Druid 连接池协作的?
答案是:通过 TransactionSynchronizationManager 和 DataSourceUtils。
4.1 事务开始时:获取连接并绑定到线程
// 类路径:org.springframework.jdbc.datasource.DataSourceTransactionManager
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
// 【核心】第一步:获取连接
Connection con = obtainDataSource().getConnection();
// 【核心】第二步:设置 autoCommit=false(重要!)
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
con.setAutoCommit(false);
}
// 【核心】第三步:将连接绑定到当前线程(重要!)
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}白话文解释:
- 获取连接:调用
obtainDataSource().getConnection(),最终会调用 Druid 的getConnection()方法,从连接池借用一个连接。 - 设置 autoCommit=false:这是事务开启的标志!设置后,后续的 SQL 都会在这个事务中执行,直到
commit()或rollback()。 - 将连接绑定到当前线程:通过
TransactionSynchronizationManager.bindResource(),把连接放到 ThreadLocal 中。这样,同一个线程中后续执行的 SQL,都会使用同一个连接(保证事务的原子性)。
4.2 事务提交时:提交事务并释放连接
// 类路径:org.springframework.jdbc.datasource.DataSourceTransactionManager
@Override
protected void doCommit(DefaultTransactionStatus status) {
// 【核心】获取连接
Connection con = txObject.getConnectionHolder().getConnection();
// 【核心】提交事务
con.commit();
}4.3 事务结束时:释放连接(归还给连接池)
// 类路径:org.springframework.jdbc.datasource.DataSourceUtils
public static void releaseConnection(Connection con, DataSource dataSource) {
// 【核心】如果连接不为 null,就释放它
if (con != null) {
try {
// 【注意】调用 connection.close(),并不是真正关闭,而是归还给连接池
con.close();
} catch (SQLException ex) {
LOG.debug("Could not close JDBC Connection", ex);
}
}
}白话文解释:
- 事务结束后,Spring 会调用
con.close()。但这个close()并不是真正关闭物理连接,而是调用DruidPooledConnection.close(),最终会调用DruidDataSource.recycle(),把连接归还给连接池。
五、埋下伏笔:MyBatis 如何与连接池协作?
说到这里,你可能会问:MyBatis 是如何与 Druid 连接池协作的?它又是如何执行 SQL 的?
这个问题,我们会在系列三(MyBatis 系列)中详细解答。这里先预告一下核心流程:
- MyBatis 通过
SqlSession执行 SQL。 SqlSession内部使用了Executor(执行器)。Executor在执行 SQL 前,会通过SpringManagedTransaction获取数据库连接。SpringManagedTransaction会通过DataSourceUtils.getConnection()获取连接。DataSourceUtils.getConnection()首先会从TransactionSynchronizationManager中获取连接(如果当前线程有事务)。- 如果当前线程没有事务,就调用
dataSource.getConnection(),也就是 Druid 的getConnection()方法。 - 获取到连接后,MyBatis 会创建
Statement(或PreparedStatement),并通过它执行 SQL。 - SQL 执行完后,MyBatis 会关闭
Statement,但不会关闭连接(因为连接是由 Spring 事务管理的)。
下一篇文章的预告:《MyBatis 系列(一):从 Mapper 接口到 SQL 执行的奇幻漂流》
六、总结与思考
今天我们一起深入了连接的生命周期与事务边界,了解了连接从创建到销毁的全过程,以及 Spring 事务是如何与 Druid 连接池协作的。
让我们用几个问题来检验一下今天的学习成果:
6.1 连接的生命周期有哪四个阶段?
- 创建(Create):Druid 调用
createPhysicalConnection()创建物理连接。 - 借用(Borrow):应用调用
getConnection()从连接池借用连接。 - 归还(Return):应用调用
connection.close(),Druid 会把连接归还给连接池(recycle())。 - 销毁(Destroy):Druid 的后台线程会定期销毁过期连接(
DestroyTask)。
6.2 事务边界是如何控制的?
- 通过
autoCommit属性控制。 - 如果
autoCommit=true(默认),每条 SQL 都是一个独立的事务。 - 如果
autoCommit=false,事务从"第一条 SQL 执行"开始,到commit()或rollback()结束。
6.3 Spring 事务是如何与 Druid 连接池协作的?
- 事务开始时:Spring 会调用
obtainDataSource().getConnection()获取连接,并设置autoCommit=false,然后把连接绑定到当前线程(通过TransactionSynchronizationManager)。 - 事务执行中:同一个线程中后续执行的 SQL,都会使用同一个连接(从
TransactionSynchronizationManager中获取)。 - 事务提交/回滚时:Spring 会调用
con.commit()或con.rollback()。 - 事务结束时:Spring 会调用
con.close(),Druid 会把连接归还给连接池。
6.4 什么是隔离级别?JDBC 有哪四种隔离级别?
隔离级别指的是:多个事务并发执行时,一个事务能"看到"另一个事务的哪些数据。
JDBC 定义了四种隔离级别:
- TRANSACTION_READ_UNCOMMITTED(读未提交):允许脏读、不可重复读、幻读。
- TRANSACTION_READ_COMMITTED(读已提交):防止脏读,但允许不可重复读、幻读。
- TRANSACTION_REPEATABLE_READ(可重复读):防止脏读、不可重复读,但允许幻读。
- TRANSACTION_SERIALIZABLE(串行化):防止脏读、不可重复读、幻读(但性能最差)。
七、扩展阅读
【扩展点】自定义事务同步回调
Spring 允许你注册事务同步回调,在事务提交后、回滚后等时间点执行一些逻辑。
// 【扩展点】注册事务同步回调
@Transactional
public void transferMoney(String fromAccount, String toAccount, BigDecimal amount) {
// 【核心】注册事务同步回调
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
// 【注意】事务提交后执行(比如发送 MQ 消息)
mqTemplate.send("transfer-completed", "Transfer completed: " + fromAccount + " -> " + toAccount);
}
@Override
public void afterCompletion(int status) {
// 【注意】事务结束后执行(提交或回滚都会执行)
if (status == STATUS_COMMITTED) {
System.out.println("Transaction committed!");
} else {
System.out.println("Transaction rolled back!");
}
}
});
// 执行转账逻辑
accountDao.debit(fromAccount, amount);
accountDao.credit(toAccount, amount);
}场景:你可以在事务提交后,发送 MQ 消息(通知其他系统"转账已完成")。这样,如果事务回滚了,MQ 消息就不会发送(保证最终一致性)。
为什么要在 afterCommit() 中发送 MQ 消息? 因为如果在事务执行中发送 MQ 消息,可能事务会回滚,但 MQ 消息已经发送了(这就导致数据不一致了)。而在 afterCommit() 中发送,就能保证:只有事务提交了,才会发送 MQ 消息。
好了,今天的文章就到这里。下一篇,我们将开始系列三:MyBatis 系列,深入 MyBatis 的源码,看看它是如何与 Spring、Druid 协作的。敬请期待!
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!你的支持是我继续创作的动力!
系列文章目录:
- 系列一:Spring 事务系列(已完成)
- 第1篇:@Transactional 注解的"前世今生"
- 第2篇:AOP 代理 —— 事务的"隐身衣"
- 第3篇:事务传播机制源码深度解析
- 第4篇:事务隔离级别的实现原理
- 第5篇:事务管理器的设计哲学
- 第6篇:事务资源的绑定与传递
- 第7篇:提交、回滚与异常规则
- 系列二:数据源系列(进行中)
- 第1篇:JDBC 规范与 DriverManager 的 legacy(已完成)
- 第2篇:Druid 连接池的整体架构(已完成)
- 第3篇:连接的借与还(getConnection 全流程)(已完成)
- 第4篇:Druid 过滤链与监控统计(已完成)
- 第5篇:MySQL 驱动的 JDBC 实现(已完成)
- 第6篇:连接的生命周期与事务边界(本文)
- 系列三:MyBatis 系列(待开始)
- 第1篇:从 Mapper 接口到 SQL 执行的奇幻漂流
- 第2篇:Executor 执行器的三种形态
- 第3篇:事务管理 —— SpringManagedTransaction 的秘密
- 第4篇:SQL 执行的全链路(StatementHandler)
- 第5篇:插件机制与责任链
- 系列四:总结系列(待开始)
- 第1篇:从 @Transactional 到 MySQL 返回的完整旅程
- 第2篇:异常全景图 —— 每个环节都可能出什么错?
- 第3篇:基于源码的调优实践
- 第4篇:那些年我们踩过的事务坑