Spring AOP引入 手写事务管理
一、转账案例
假设有一个最简单的转账业务:
public void transfer(String sourceName, String targetName, Double money) {
Account sourceAccount = accountDao.findAccountByName(sourceName); // 操作1
Account targetAccount = accountDao.findAccountByName(targetName); // 操作2
sourceAccount.setMoney(sourceAccount.getMoney() - money);
targetAccount.setMoney(targetAccount.getMoney() + money);
accountDao.updateAccount(sourceAccount); // 操作3:扣钱
int i = 1 / 0; // 模拟业务异常
accountDao.updateAccount(targetAccount); // 操作4:加钱(不会执行到)
}
运行之后你会发现:扣钱成功了,但加钱没有执行。钱就这么凭空消失了。
原因是:操作 3 和操作 4 分别从连接池拿了两个不同的 Connection,各自自动提交,根本不在同一个事务里。出了异常,操作 3 已经提交,操作 4 没有执行,无法回滚。
二、问题根源分析
要让事务生效,必须满足一个前提:同一个事务内的所有数据库操作,必须使用同一个 Connection。
但 Service 调用 DAO 的多个方法时,每个方法内部都会单独去连接池拿连接,很难保证拿到同一个。
最直接的解法是手动传连接:
// 非常丑陋的写法
public void transfer(...) {
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
accountDao.findAccountByName(conn, sourceName); // 手动传 conn
accountDao.updateAccount(conn, sourceAccount); // 手动传 conn
conn.commit();
}
这种写法侵入性极强,而且 Service 层直接持有 Connection,违反了分层原则。
更优雅的做法:用 ThreadLocal 把连接绑定到当前线程上,让 DAO 自己去线程里取。
三、解决方案:两个工具类
3.1 ConnectionUtils:连接与线程绑定
/**
* 连接工具类:从数据源获取连接,并与当前线程绑定
*/
public class ConnectionUtils {
// 关键:ThreadLocal 变量,全局只有一个实例
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 获取与当前线程绑定的连接
* 如果没有,就从连接池拿一个并绑定到当前线程
*/
public Connection getThreadConnection() {
try {
Connection connection = tl.get(); // 先从当前线程的口袋里找
if (connection == null) {
connection = dataSource.getConnection(); // 没有才去连接池拿
tl.set(connection); // 存入当前线程的口袋
}
return connection;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 将当前线程与连接解绑
*/
public void removeConnection() {
tl.remove();
}
}
核心逻辑:第一次调用时去连接池拿,并存到当前线程;之后再调用,直接从线程里取。
同一个线程里无论调用多少次 getThreadConnection(),拿到的永远是同一个 Connection。
3.2 TransactionManager:事务控制器
/**
* 事务管理工具类:开启、提交、回滚、释放
*/
public class TransactionManager {
private ConnectionUtils connectionUtils;
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
// 开启事务:关闭自动提交
public void beginTransaction() {
try {
connectionUtils.getThreadConnection().setAutoCommit(false);
} catch (Exception e) {
e.printStackTrace();
}
}
// 提交事务
public void commit() {
try {
connectionUtils.getThreadConnection().commit();
} catch (Exception e) {
e.printStackTrace();
}
}
// 回滚事务
public void rollback() {
try {
connectionUtils.getThreadConnection().rollback();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 释放资源
* 注意:close() 只是把连接还给连接池,线程和连接的绑定还在
* 必须调用 removeConnection() 彻底解绑,否则下次这个线程被复用时会拿到旧连接
*/
public void release() {
try {
connectionUtils.getThreadConnection().close(); // 还给连接池
connectionUtils.removeConnection(); // 解除线程绑定
} catch (Exception e) {
e.printStackTrace();
}
}
}
四、改造 DAO 层
DAO 不再从连接池拿连接,改为从 ConnectionUtils 拿当前线程绑定的连接:
public class AccountDaoImpl implements AccountDao {
private QueryRunner runner;
private ConnectionUtils connectionUtils; // 注入工具类
public Account findAccountByName(String name) {
try {
// 关键:传入当前线程的连接,而不是让 runner 自己去连接池拿
List<Account> list = runner.query(
connectionUtils.getThreadConnection(), // 从线程口袋里取连接
"select * from account where name=?",
new BeanListHandler<Account>(Account.class),
name
);
if (list == null || list.size() == 0) return null;
if (list.size() > 1) throw new RuntimeException("结果不唯一");
return list.get(0);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void updateAccount(Account account) {
try {
runner.update(
connectionUtils.getThreadConnection(), // 同一个线程,同一个连接
"update account set money=? where name=?",
account.getMoney(),
account.getName()
);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
五、改造 Service 层
Service 层负责用 TransactionManager 包裹业务逻辑:
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao;
private TransactionManager transactionManager; // 注入事务管理器
public void transfer(String sourceName, String targetName, Double money) {
try {
transactionManager.beginTransaction(); // 开启事务(关闭自动提交)
Account sourceAccount = accountDao.findAccountByName(sourceName);
Account targetAccount = accountDao.findAccountByName(targetName);
sourceAccount.setMoney(sourceAccount.getMoney() - money);
targetAccount.setMoney(targetAccount.getMoney() + money);
accountDao.updateAccount(sourceAccount); // 扣钱
// int i = 1 / 0; // 取消注释可以测试事务回滚
accountDao.updateAccount(targetAccount); // 加钱
transactionManager.commit(); // 提交事务
} catch (Exception e) {
e.printStackTrace();
transactionManager.rollback(); // 出异常,回滚
} finally {
transactionManager.release(); // 无论如何,释放连接
}
}
}
六、整体流程
以一次转账请求为例,假设由线程 Thread-007 执行,逐步拆解每个环节发生了什么。
第一步:开启事务
transactionManager.beginTransaction() 内部调用 connectionUtils.getThreadConnection()。此时 Thread-007 的 ThreadLocalMap 里还没有连接,于是从连接池拿一个 Connection-A,存入 Thread-007 的 ThreadLocalMap,再执行 Connection-A.setAutoCommit(false) 关闭自动提交,事务正式开启。
第二步:查询转出账户
accountDao.findAccountByName(sourceName) 内部再次调用 getThreadConnection()。这次 Thread-007 的口袋里已经有 Connection-A 了,直接返回,不再去连接池拿新的。
第三步:查询转入账户
同上,accountDao.findAccountByName(targetName) 拿到的依然是 Connection-A。
第四步:执行扣钱和加钱
accountDao.updateAccount(sourceAccount) 和 accountDao.updateAccount(targetAccount) 两次更新操作,拿到的都是 Connection-A。此时四次数据库操作全部在同一个连接、同一个事务里执行。
第五步:提交事务
业务正常完成,transactionManager.commit() 对 Connection-A 执行 commit(),四步操作一次性全部提交。若中途抛出异常,则走 rollback(),所有操作一并撤销。
第六步:释放资源
transactionManager.release() 先把 Connection-A 还给连接池,再调用 tl.remove() 解除 Thread-007 与连接的绑定,避免线程被复用时拿到脏连接。
四次数据库操作,用的是同一个 Connection-A,在同一个事务里,要么全成功,要么全回滚。
七、多线程并发下会怎样?
ConnectionUtils 和 TransactionManager 在 Spring 中都是单例的。你可能担心:单例对象下多线程并发会不会乱套?
完全不会,这正是 ThreadLocal 的精妙之处。
| 维度 | 状态 |
|---|---|
| ConnectionUtils 实例 | 单例,全局共享 |
| ThreadLocal 实例 (tl) | 单例,全局共享 |
| Connection 对象 | 每个线程各有一个,隔离 |
| ThreadLocalMap | 每个 Thread 对象私有,隔离 |
假设线程 A 和线程 B 同时发起转账:
- 线程 A 调用
tl.get():JVM 找到线程 A 自己的 ThreadLocalMap,取出 Connection-A - 线程 B 调用
tl.get():JVM 找到线程 B 自己的 ThreadLocalMap,取出 Connection-B
虽然调用的是同一个 tl 对象的同一个 get() 方法,但 JVM 自动根据"谁在调用"来决定去哪个线程的 Map 里取数据。两个线程的连接完全隔离,互不干扰。
结论:单例工具类 + ThreadLocal = 逻辑统一管理,数据物理隔离。
八、release() 里为什么要调用 removeConnection()?
这是一个很容易被忽略的细节。
connection.close() 在连接池场景下,并不是真正关闭连接,而是把连接还给连接池。还回去之后,这个 Connection 对象可能被下一个请求复用。
但问题是:tl.remove() 如果不调用,当前线程的 ThreadLocalMap 里还存着对这个 Connection 的引用。
当这个线程(来自线程池)被复用来处理下一个请求时,调用 tl.get() 会发现口袋里"有连接",但那个连接可能已经被其他线程拿走用了,导致数据污染,严重时出现事务混乱。
所以 release() 的正确顺序是:
connection.close(); // 1. 先把连接还给连接池
tl.remove(); // 2. 再解除线程与连接的绑定
九、总结
这套手写事务的核心思路只有一点:
用 ThreadLocal 把 Connection 绑定到线程上,让同一个线程内的所有 DAO 操作共享同一个连接,从而实现事务控制。
其实这正是 Spring 事务管理的底层原理。Spring 的 @Transactional 注解背后,TransactionSynchronizationManager 就是用 ThreadLocal 来存储当前线程的连接资源的——只不过它帮你把这些繁琐的工作都自动化了。
理解了这套手写版本,再去看 Spring 的声明式事务,会清晰很多。
下一篇:ThreadLocal 原理深度解析
更多推荐



所有评论(0)