小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
文章涉及的源码均已上传到了码云,参考【README.md】文件部署运行即可
手写系列码云地址: git@gitee.com:tangjingshan/tjs-study-m…
本文代码路径:{@link tjs.styudy.mini.springboot.demo.transaction.DoTestOfAppplaction#main}
前言
本文主要是总结下如何手写声明式事务@Transactional的mini版本,至于其具体的源码实现,可以参考之前的博客
手写声明式事务@Transactional(一):手把手解析源码(配gif动态图)
一. 如何运行demo
- 执行以下脚本,创建所需的数据库
DROP TABLE IF EXISTS `user_test`;
CREATE TABLE `user_test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_name` varchar(255) DEFAULT NULL,
`balance` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
INSERT INTO `user_test` VALUES (1, 'a', 10);
INSERT INTO `user_test` VALUES (2, 'b', 20);
-
引入文章开头中的码云项目
-
找到测试类
tjs.styudy.mini.springboot.demo.transaction.DoTestOfAppplaction#main
-
更改测试类中的数据源信息
-
观察测试代码所做之事 updateSonOfBalance调用updateFatherOfUserName方法,其中updateFatherOfUserName事物传播级别为
Propagation.REQUIRES_NEW)
,会重新创建一个新的事物
@MiniTransactional
@Service
public class TransactionalService {
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
TransactionalService transactionalService;
//@MiniTransactional(propagation = Propagation.REQUIRED)
public void updateSonOfBalance(boolean isThrowException) {
jdbcTemplate.execute("UPDATE user_test ut SET ut.balance = ut.balance + 1 WHERE ut.id = 1;");
// 注入的bean,才能走代理
transactionalService.updateFatherOfUserName(isThrowException);
// 直接调用updateFatherOfUserName,updateFatherOfUserName是不会走代理的
// 但是如果此前,已经绑定了已关闭自动提交的Connection到ThreadLocal,则即使没有走代理,也会是在同一事物下
// this.updateFatherOfUserName(isThrowException);
System.out.println("updateFatherOfUserName已执行完成,数据已入库。。。");
}
@MiniTransactional(propagation = Propagation.REQUIRES_NEW)//创建一个新事务,如果当前存在事务,将这个事务挂起
public void updateFatherOfUserName(boolean isThrowException) {
// 如果隔离级别为 Propagation.REQUIRES_NEW
// 由于updateSonOfBalance修改了id为1的数据,但是并没有提交事务,此时id为1的那行数据仍旧在加锁状态,尚未释放
// 此时再去修改id为1的数据,就会产生死锁。
//jdbcTemplate.execute("UPDATE user_test ut SET ut.user_name = CONCAT(ut.user_name, "_", ut.balance+1) WHERE ut.id = 1;");
jdbcTemplate.execute("UPDATE user_test ut SET ut.user_name = CONCAT(ut.user_name, "_", ut.balance+1) WHERE ut.id = 2;");
if (isThrowException) {
throw new RuntimeException("test rollback.....");
}
}
public void find(String pre) {
List<JSONObject> userTests = jdbcTemplate.query("select * from user_test ut where ut.id = 1;");
System.out.println(pre + ":" + JSON.toJSONString(userTests));
}
}
初始化脚本后,回滚成功与否标识如下
-
回滚成功
-
回滚失败
- 注释掉
@MiniTransactional
注解,演示回滚失败场景
- 加上
@MiniTransactional
注解,演示回滚成功场景
- 加上
@MiniTransactional
注解,演示分段提交成功场景 更改启动类为不报错,并在以下位置打上断点
由于updateFatherOfUserName
使用的是新的Connection,所以updateFatherOfUserName
方法内对数据库的操作应该在updateFatherOfUserName
方法调用完后就入库了
至此,mini版声明式事务 @MiniTransactional 已能满足大部分单机应用的使用场景,至于分布式事务,还需解决如何跨服务触发提交/回滚事务的问题,后续再总结。
二. 分析待做事项
通过上一篇博客的分析,可知声明式事务@Transactional主要做的事情如下
- 加载相关配置类到IOC
- 切面拦截事务,切面所作之事,伪代码如下
//1. 实例化TransactionInfo
try {
//2. 调用目标方法
} catch (Exception ex) {
//3.1 回滚事务,释放资源
throw ex;
}
//3.2 提交事务,释放资源
- 编写
JdbcTemplate
- 编写测试类
三. 开始手写
接下来就按着上面的分析,开始手写
具体的手写代码见码云项目,代码量较多这里就不贴了
这里就贴一下关键节点的代码位置
/**
* 切面类:{@link tjs.styudy.mini.springboot.transaction.aop.TransactionAspect#around(org.aspectj.lang.ProceedingJoinPoint)}
* 提交事务:{@link tjs.styudy.mini.springboot.transaction.config.TransactionManager#commit(tjs.styudy.mini.springboot.transaction.config.TransactionInfo)}
* 回滚事务:{@link tjs.styudy.mini.springboot.transaction.config.TransactionManager#rollBack(tjs.styudy.mini.springboot.transaction.config.TransactionInfo)}
* 清理资源:{@link tjs.styudy.mini.springboot.transaction.config.TransactionManager#cleanupAfterCompletion()}
* 获取当前连接:{@link tjs.styudy.mini.springboot.transaction.config.ConnectionHolder#getResource()}
*/
四. 遇到的问题
4.1 Propagation.REQUIRES_NEW导致死锁
原因:
updateSonOfBalance
的ConnectionA,修改了id为1的那行数据,对其加上了行锁updateFatherOfUserName
的ConnectionB,也准备修改id为1的那行数据,但是updateSonOfBalance
事务还未提交,行锁还未释放- 最终导致死锁,等到超时 此种场景不只于mini版本,spring的版本也会有此问题
4.2 还原ThreadLocal
要做的事情:
- 进入
updateSonOfBalance
,ThreadLocal当前值为ConnectionA - 进入
updateFatherOfUserName
,ThreadLocal当前值为ConnectionB - 结束
updateFatherOfUserName
,ThreadLocal当前值还原为ConnectionA
如何还原?mini版本(spring也是类似的做法)是存入ThreadLocal的值为一个对象tjs.styudy.mini.springboot.transaction.config.ConnectionHolder
,其有两个属性
每次方法快结束,释放资源时,将ThreadLocal当前值重置为上一个方法的ConnectionHolder
这样就可以同时满足以下两种需求
-
开启新事物的方法,在结束调用时,还原为上一个方法的Connection 结束
updateFatherOfUserName
,ThreadLocal当前值还原为ConnectionA -
顶层方法,在结束调用时,会将ThreadLocal当前值置为null,间接清理了资源 结束
updateSonOfBalance
,ThreadLocal当前值还原为null
4.3 处理无注解的情况
如果没有标识注解,则应该直接从连接池中拿连接,用完后直接还回去,JdbcTemplate
代码如下
public void execute(String sql) {
// 标识MiniTransactional注解,从threadLocal中获取当前连接
Connection connection = ConnectionHolder.getCurConnectionStatic();
if (connection != null) {
try (Statement stmt = connection.createStatement()) {
stmt.execute(sql);
return;
} catch (SQLException throwables) {
throw new RuntimeException(throwables);
}
}
// 没有标识MiniTransactional注解,从连接池中获取当前连接
try (Connection connectionNew = this.dataSource.getConnection();
Statement stmtNew = connectionNew.createStatement()) {
stmtNew.execute(sql);
} catch (SQLException throwables) {
throw new RuntimeException(throwables);
}
}