我为了准备美团的面试,爆肝60张图画明白了Spring事务!
点击上方“芋道源码”,选择“设为星标”
管她前浪,还是后浪?
能浪的浪,才是好浪!
每天 10:33 更新文章,每天掉亿点点头发...
源码精品专栏
本文会简单介绍一下 Spring 事务的基础知识,以及使用方法,然后直接对源码进行拆解。
不 BB,上文章目录。
1. 项目准备
需要搭建环境的同学,代码详见 :https://github.com/lml200701158/program_demo/tree/main/spring-transaction
下面是 DB 数据和 DB 操作接口:
uid | uname | usex |
---|---|---|
1 | 张三 | 女 |
2 | 陈恒 | 男 |
3 | 楼仔 | 男 |
// 提供的接口
public interface UserDao {
// select * from user_test where uid = "#{uid}"
public MyUser selectUserById(Integer uid);
// update user_test set uname =#{uname},usex = #{usex} where uid = #{uid}
public int updateUser(MyUser user);
}
基础测试代码,testSuccess() 是事务生效的情况:
@Service
public class Louzai {
@Autowired
private UserDao userDao;
public void update(Integer id) {
MyUser user = new MyUser();
user.setUid(id);
user.setUname("张三-testing");
user.setUsex("女");
userDao.updateUser(user);
}
public MyUser query(Integer id) {
MyUser user = userDao.selectUserById(id);
return user;
}
// 正常情况
@Transactional(rollbackFor = Exception.class)
public void testSuccess() throws Exception {
Integer id = 1;
MyUser user = query(id);
System.out.println("原记录:" + user);
update(id);
throw new Exception("事务生效");
}
}
执行入口:
public class SpringMyBatisTest {
public static void main(String[] args) throws Exception {
String xmlPath = "applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
Louzai uc = (Louzai) applicationContext.getBean("louzai");
uc.testSuccess();
}
}
输出:
16:44:38.267 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.transaction.interceptor.TransactionInterceptor#0'
16:44:38.363 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'txManager'
16:44:40.966 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Creating new transaction with name [com.mybatis.controller.Louzai.testSuccess]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-java.lang.Exception
16:44:40.968 [main] DEBUG org.springframework.jdbc.datasource.DriverManagerDataSource - Creating new JDBC DriverManager Connection to [jdbc:mysql://127.0.0.1:3306/java_study?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai]
16:44:41.228 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@5b5caf08] for JDBC transaction
16:44:41.231 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5b5caf08] to manual commit
原记录:MyUser(uid=1, uname=张三, usex=女)
16:42:59.345 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Initiating transaction rollback
16:42:59.346 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@70807224]
16:42:59.354 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@70807224] after transaction
Exception in thread "main" java.lang.Exception: 事务生效
at com.mybatis.controller.Louzai.testSuccess(Louzai.java:34)
// 异常日志省略...
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/ruoyi-vue-pro 视频教程:https://doc.iocoder.cn/video/
2. Spring 事务工作流程
为了方便大家能更好看懂后面的源码,我先整体介绍一下源码的执行流程,让大家有一个整体的认识,否则容易被绕进去。
整个 Spring 事务源码,其实分为 2 块,我们会结合上面的示例,给大家进行讲解。
第一块是后置处理 ,我们在创建 Louzai Bean 的后置处理器中,里面会做两件事情:
获取 Louzai 的切面方法 :首先会拿到所有的切面信息,和 Louzai 的所有方法进行匹配,然后找到 Louzai 所有需要进行事务处理的方法,匹配成功的方法,还需要将事务属性保存到缓存 attributeCache 中。
创建 AOP 代理对象 :结合 Louzai 需要进行 AOP 的方法,选择 Cglib 或 JDK,创建 AOP 代理对象。
第二块是事务执行 ,整个逻辑比较复杂,我只选取 4 块最核心的逻辑,分别为从缓存拿到事务属性、创建并开启事务、执行业务逻辑、提交或者回滚事务。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/yudao-cloud 视频教程:https://doc.iocoder.cn/video/
3. 源码解读
注意:Spring 的版本是 5.2.15.RELEASE,否则和我的代码不一样!!!
上面的知识都不难,下面才是我们的重头戏,让你跟着楼仔,走一遍代码流程。
3.1 代码入口
这里需要多跑几次,把前面的 beanName 跳过去,只看 louzai。
进入 doGetBean(),进入创建 Bean 的逻辑。
进入 createBean(),调用 doCreateBean()。
进入 doCreateBean(),调用 initializeBean()。
如果看过我前面几期系列源码的同学,对这个入口应该会非常熟悉,其实就是用来创建代理对象。
3.2 创建代理对象
这里是重点!敲黑板!!!
先获取 louzai 类的所有切面列表; 创建一个 AOP 的代理对象。
3.2.1 获取切面列表
这里有 2 个重要的方法,先执行 findCandidateAdvisors(),待会我们还会再返回 findEligibleAdvisors()。
依次返回,重新来到 findEligibleAdvisors()。
进入 canApply(),开始匹配 louzai 的切面。
这里是重点!敲黑板!!!
这里只会匹配到 Louzai.testSuccess() 方法,我们直接进入匹配逻辑。
如果匹配成功,还会把事务的属性配置信息放入 attributeCache 缓存。
我们依次返回到 getTransactionAttribute(),再看看放入缓存中的数据。
再回到该小节开头,我们拿到 louzai 的切面信息,去创建 AOP 代理对象。
3.2.2 创建 AOP 代理对象
创建 AOP 代理对象的逻辑,在上一篇文章(Spring AOP)讲解过,我是通过 Cglib 创建。
3.3 事务执行
回到业务逻辑,通过 louzai 的 AOP 代理对象 ,开始执行主方法。
因为代理对象是 Cglib 方式创建,所以通过 Cglib 来执行。
这里是重点!敲黑板!!!
下面的代码是事务执行的核心逻辑 invokeWithinTransaction()。
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
//获取我们的事务属源对象
TransactionAttributeSource tas = getTransactionAttributeSource();
//通过事务属性源对象获取到我们的事务属性信息
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
//获取我们配置的事务管理器对象
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
//从tx属性对象中获取出标注了@Transactionl的方法描述符
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
//处理声明式事务
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
//有没有必要创建事务
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal;
try {
//调用钩子函数进行回调目标方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
//抛出异常进行回滚处理
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
//清空我们的线程变量中transactionInfo的值
cleanupTransactionInfo(txInfo);
}
//提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
//编程式事务
else {
// 这里不是我们的重点,省略...
}
}
3.3.1 获取事务属性
在 invokeWithinTransaction() 中,我们找到获取事务属性的入口。
从 attributeCache 获取事务的缓存数据,缓存数据是在 “2.2.1 获取切面列表” 中保存的。
3.3.2 创建事务
通过 doGetTransaction() 获取事务。
protected Object doGetTransaction() {
//创建一个数据源事务对象
DataSourceTransactionObject txObject = new DataSourceTransactionObject();
//是否允许当前事务设置保持点
txObject.setSavepointAllowed(isNestedTransactionAllowed());
/**
* TransactionSynchronizationManager 事务同步管理器对象(该类中都是局部线程变量)
* 用来保存当前事务的信息,我们第一次从这里去线程变量中获取 事务连接持有器对象 通过数据源为key去获取
* 由于第一次进来开始事务 我们的事务同步管理器中没有被存放.所以此时获取出来的conHolder为null
*/
ConnectionHolder conHolder =
(ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());
txObject.setConnectionHolder(conHolder, false);
//返回事务对象
return txObject;
}
通过 startTransaction() 开启事务。
下面是开启事务的详细逻辑,了解一下即可。
protected void doBegin(Object transaction, TransactionDefinition definition) {
//强制转化事务对象
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;
try {
//判断事务对象没有数据库连接持有器
if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
//通过数据源获取一个数据库连接对象
Connection newCon = obtainDataSource().getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
//把我们的数据库连接包装成一个ConnectionHolder对象 然后设置到我们的txObject对象中去
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
//标记当前的连接是一个同步事务
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();
//为当前的事务设置隔离级别
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
//关闭自动提交
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
con.setAutoCommit(false);
}
//判断事务为只读事务
prepareTransactionalConnection(con, definition);
//设置事务激活
txObject.getConnectionHolder().setTransactionActive(true);
//设置事务超时时间
int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}
// 绑定我们的数据源和连接到我们的同步管理器上 把数据源作为key,数据库连接作为value 设置到线程变量中
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
}
catch (Throwable ex) {
if (txObject.isNewConnectionHolder()) {
//释放数据库连接
DataSourceUtils.releaseConnection(con, obtainDataSource());
txObject.setConnectionHolder(null, false);
}
throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
}
}
最后返回到 invokeWithinTransaction(),得到 txInfo 对象。
3.3.3 执行逻辑
还是在 invokeWithinTransaction() 中,开始执行业务逻辑。
进入到真正的业务逻辑。
执行完毕后抛出异常,依次返回,走后续的回滚事务逻辑。
3.3.4 回滚事务
还是在 invokeWithinTransaction() 中,进入回滚事务的逻辑。
执行回滚逻辑很简单,我们只看如何判断是否回滚。
如果抛出的异常类型,和事务定义的异常类型匹配,证明该异常需要捕获。
之所以用递归,不仅需要判断抛出异常的本身,还需要判断它继承的父类异常,满足任意一个即可捕获。
到这里,所有的流程结束。
4. 结语
我们再小结一下,文章先介绍了事务的使用示例,以及事务的执行流程。
之后再剖析了事务的源码,分为 2 块:
先匹配出 louzai 对象所有关于事务的切面列表,并将匹配成功的事务属性保存到缓存; 从缓存取出事务属性,然后创建、启动事务,执行业务逻辑,最后提交或者回滚事务。
欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:
已在知识星球更新源码解析如下:
最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。
提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。
获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。
文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
微信扫码关注该文公众号作者