Redian新闻
>
我为了准备美团的面试,爆肝60张图画明白了Spring事务!

我为了准备美团的面试,爆肝60张图画明白了Spring事务!

公众号新闻

点击上方“芋道源码”,选择“设为星标

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 
来源:楼仔

本文会简单介绍一下 Spring 事务的基础知识,以及使用方法,然后直接对源码进行拆解。

不 BB,上文章目录。

1. 项目准备

需要搭建环境的同学,代码详见 :https://github.com/lml200701158/program_demo/tree/main/spring-transaction

下面是 DB 数据和 DB 操作接口:

uidunameusex
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 创建代理对象

这里是重点!敲黑板!!!

  1. 先获取 louzai 类的所有切面列表;
  2. 创建一个 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(nullfalse);
        }
        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 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
日本最沙雕的cosplay大会来了,这20张图简直神还原带着老公的不育证明面试,当着hr的面出柜,她这是酷?Me & Master 专篇|粟叶 & 矶崎新+胡倩事务所:在大师事务所接触了远大于建筑学的世界访拜登总统的母校雪城大学野!Penn State德州King上线,被这个Morgan Stanley的面试官秀到了秋日游布鲁克林海滩Spring for Apache Kafka 3.0 和 Spring for RabbitMQ 3.0 发布被GPT带飞的In-Context Learning发展现状如何?这篇综述梳理明白了Spotlight Spring Savings 10月19日-11月6日打折图册(高清29页)对话 Spring 大神:Spring 生态系统的新时代来了!在佛罗里达买房的风险为了职业生涯,我要经历一场4天3夜的面试|2022超人学院·乐活青创营开战倒计时天气很凉,但美团的广告创意不会“凉”Hadoop/Spark 太重,esProc SPL 很轻明日开讲 | 大厂在职面试官,破译低迷市场下的面试难点!The Opioid Crisis the Double Abuses用Prometheus监控K8s,从核心原理到告警实操都讲明白了 | 极客时间别再逼AI画美少女吃面条了,这20张图让我忘了筷子怎么用这些把人问懵的面试题怎么答? | 面试必看美国朝鲜事务特别代表金聖与中国朝鲜半岛事务特别代表刘晓明举行视频会议我最近做了个测试,终于明白了为何人人都感觉自己“累成狗”长文捋明白Spring事务!隔离性?传播性?一网打尽!证监会连发10张罚单,事关投行业务!美团的外卖,能送到香港吗?美团的即时零售,走不出北上广深马上直播 | 大厂在职面试官,破译低迷市场下的面试难点!冬天的美国中西部,这10张图解释的明明白白!字节鏖战美团的关键一役EVA粉必入!台湾角川《EVA漫画BOX完全版》带真本义行原画明信片!美国进入了低效率的时代?[电脑] 终究入手了Studio Display,盘点我用过十分满意的摄影摄像周边美团的尽头是山姆?美本早申请提交后,你收到梦校的面试邀约了嘛?该如何准备增加录取概率?发现一个Spring事务的巨坑bug,可是官方都不承认?大家来评评理!官宣!小米13系列周日见/《三体》动画明天开播/苹果iCloud将支持端到端加密
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。