• 61张图,图解Spring事务,拆解底层源码


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

    不 BB,上文章目录。

    ​1. 项目准备

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

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

    uidunameusex1张三女2陈恒男3楼仔男

    1. // 提供的接口
    2. public interface UserDao {
    3. // select * from user_test where uid = "#{uid}"
    4. public MyUser selectUserById(Integer uid);
    5. // update user_test set uname =#{uname},usex = #{usex} where uid = #{uid}
    6. public int updateUser(MyUser user);
    7. }

    基础测试代码,testSuccess() 是事务生效的情况:

    1. @Service
    2. public class Louzai {
    3. @Autowired
    4. private UserDao userDao;
    5. public void update(Integer id) {
    6. MyUser user = new MyUser();
    7. user.setUid(id);
    8. user.setUname("张三-testing");
    9. user.setUsex("女");
    10. userDao.updateUser(user);
    11. }
    12. public MyUser query(Integer id) {
    13. MyUser user = userDao.selectUserById(id);
    14. return user;
    15. }
    16. // 正常情况
    17. @Transactional(rollbackFor = Exception.class)
    18. public void testSuccess() throws Exception {
    19. Integer id = 1;
    20. MyUser user = query(id);
    21. System.out.println("原记录:" + user);
    22. update(id);
    23. throw new Exception("事务生效");
    24. }
    25. }

    执行入口:

    1. public class SpringMyBatisTest {
    2. public static void main(String[] args) throws Exception {
    3. String xmlPath = "applicationContext.xml";
    4. ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
    5. Louzai uc = (Louzai) applicationContext.getBean("louzai");
    6. uc.testSuccess();
    7. }
    8. }

    输出:

    1. 16:44:38.267 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.transaction.interceptor.TransactionInterceptor#0'
    2. 16:44:38.363 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'txManager'
    3. 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
    4. 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]
    5. 16:44:41.228 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@5b5caf08] for JDBC transaction
    6. 16:44:41.231 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5b5caf08] to manual commit
    7. 原记录:MyUser(uid=1, uname=张三, usex=女)
    8. 16:42:59.345 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Initiating transaction rollback
    9. 16:42:59.346 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@70807224]
    10. 16:42:59.354 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@70807224] after transaction
    11. Exception in thread "main" java.lang.Exception: 事务生效
    12. at com.mybatis.controller.Louzai.testSuccess(Louzai.java:34)
    13. // 异常日志省略...

    2. Spring 事务工作流程

    为了方便大家能更好看懂后面的源码,我先整体介绍一下源码的执行流程,让大家有一个整体的认识,否则容易被绕进去。

    整个 Spring 事务源码,其实分为 2 块,我们会结合上面的示例,给大家进行讲解。

    ​第一块是后置处理,我们在创建 Louzai Bean 的后置处理器中,里面会做两件事情:

    获取 Louzai 的切面方法:首先会拿到所有的切面信息,和 Louzai 的所有方法进行匹配,然后找到 Louzai 所有需要进行事务处理的方法,匹配成功的方法,还需要将事务属性保存到缓存 attributeCache 中。

    创建 AOP 代理对象:结合 Louzai 需要进行 AOP 的方法,选择 Cglib 或 JDK,创建 AOP 代理对象。

    ​第二块是事务执行,整个逻辑比较复杂,我只选取 4 块最核心的逻辑,分别为从缓存拿到事务属性、创建并开启事务、执行业务逻辑、提交或者回滚事务。

    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()。

    1. protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
    2. final InvocationCallback invocation) throws Throwable {
    3. //获取我们的事务属源对象
    4. TransactionAttributeSource tas = getTransactionAttributeSource();
    5. //通过事务属性源对象获取到我们的事务属性信息
    6. final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
    7. //获取我们配置的事务管理器对象
    8. final PlatformTransactionManager tm = determineTransactionManager(txAttr);
    9. //从tx属性对象中获取出标注了@Transactionl的方法描述符
    10. final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
    11. //处理声明式事务
    12. if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
    13. //有没有必要创建事务
    14. TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
    15. Object retVal;
    16. try {
    17. //调用钩子函数进行回调目标方法
    18. retVal = invocation.proceedWithInvocation();
    19. }
    20. catch (Throwable ex) {
    21. //抛出异常进行回滚处理
    22. completeTransactionAfterThrowing(txInfo, ex);
    23. throw ex;
    24. }
    25. finally {
    26. //清空我们的线程变量中transactionInfo的值
    27. cleanupTransactionInfo(txInfo);
    28. }
    29. //提交事务
    30. commitTransactionAfterReturning(txInfo);
    31. return retVal;
    32. }
    33. //编程式事务
    34. else {
    35. // 这里不是我们的重点,省略...
    36. }
    37. }

    3.3.1 获取事务属性

    在 invokeWithinTransaction() 中,我们找到获取事务属性的入口。

    ​从 attributeCache 获取事务的缓存数据,缓存数据是在 “2.2.1 获取切面列表” 中保存的。

    3.3.2 创建事务

    ​通过 doGetTransaction() 获取事务。

    1. protected Object doGetTransaction() {
    2. //创建一个数据源事务对象
    3. DataSourceTransactionObject txObject = new DataSourceTransactionObject();
    4. //是否允许当前事务设置保持点
    5. txObject.setSavepointAllowed(isNestedTransactionAllowed());
    6. /**
    7. * TransactionSynchronizationManager 事务同步管理器对象(该类中都是局部线程变量)
    8. * 用来保存当前事务的信息,我们第一次从这里去线程变量中获取 事务连接持有器对象 通过数据源为key去获取
    9. * 由于第一次进来开始事务 我们的事务同步管理器中没有被存放.所以此时获取出来的conHolder为null
    10. */
    11. ConnectionHolder conHolder =
    12. (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());
    13. txObject.setConnectionHolder(conHolder, false);
    14. //返回事务对象
    15. return txObject;
    16. }

    通过 startTransaction() 开启事务。

    下面是开启事务的详细逻辑,了解一下即可。

    1. protected void doBegin(Object transaction, TransactionDefinition definition) {
    2. //强制转化事务对象
    3. DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
    4. Connection con = null;
    5. try {
    6. //判断事务对象没有数据库连接持有器
    7. if (!txObject.hasConnectionHolder() ||
    8. txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
    9. //通过数据源获取一个数据库连接对象
    10. Connection newCon = obtainDataSource().getConnection();
    11. if (logger.isDebugEnabled()) {
    12. logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
    13. }
    14. //把我们的数据库连接包装成一个ConnectionHolder对象 然后设置到我们的txObject对象中去
    15. txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
    16. }
    17. //标记当前的连接是一个同步事务
    18. txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
    19. con = txObject.getConnectionHolder().getConnection();
    20. //为当前的事务设置隔离级别
    21. Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
    22. txObject.setPreviousIsolationLevel(previousIsolationLevel);
    23. //关闭自动提交
    24. if (con.getAutoCommit()) {
    25. txObject.setMustRestoreAutoCommit(true);
    26. if (logger.isDebugEnabled()) {
    27. logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
    28. }
    29. con.setAutoCommit(false);
    30. }
    31. //判断事务为只读事务
    32. prepareTransactionalConnection(con, definition);
    33. //设置事务激活
    34. txObject.getConnectionHolder().setTransactionActive(true);
    35. //设置事务超时时间
    36. int timeout = determineTimeout(definition);
    37. if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
    38. txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
    39. }
    40. // 绑定我们的数据源和连接到我们的同步管理器上 把数据源作为key,数据库连接作为value 设置到线程变量中
    41. if (txObject.isNewConnectionHolder()) {
    42. TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
    43. }
    44. }
    45. catch (Throwable ex) {
    46. if (txObject.isNewConnectionHolder()) {
    47. //释放数据库连接
    48. DataSourceUtils.releaseConnection(con, obtainDataSource());
    49. txObject.setConnectionHolder(null, false);
    50. }
    51. throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
    52. }
    53. }

    CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex); } }

    最后返回到 invokeWithinTransaction(),得到 txInfo 对象。

    3.3.3 执行逻辑

    还是在 invokeWithinTransaction() 中,开始执行业务逻辑。

    ​进入到真正的业务逻辑。

    ​执行完毕后抛出异常,依次返回,走后续的回滚事务逻辑。

    3.3.4 回滚事务

    还是在 invokeWithinTransaction() 中,进入回滚事务的逻辑。

    ​执行回滚逻辑很简单,我们只看如何判断是否回滚。

    如果抛出的异常类型,和事务定义的异常类型匹配,证明该异常需要捕获。

    之所以用递归,不仅需要判断抛出异常的本身,还需要判断它继承的父类异常,满足任意一个即可捕获。

    ​到这里,所有的流程结束。

    4. 结语

    我们再小节一下,文章先介绍了事务的使用示例,以及事务的执行流程。

    之后再剖析了事务的源码,分为 2 块:

    • 先匹配出 louzai 对象所有关于事务的切面列表,并将匹配成功的事务属性保存到缓存;

    • 从缓存取出事务属性,然后创建、启动事务,执行业务逻辑,最后提交或者回滚事务。

  • 相关阅读:
    900个开源AI工具背后,我看到的趋势
    电脑重装系统后Win11用户名怎么更改
    DP读书:《openEuler操作系统》(四)鲲鹏处理器
    MySQL报错:json_contains: “The document is empty.“ at position 0.
    数据集:机器人理解世界的关键
    g2o中的边Edge
    单片机采集传感器数据(整形,浮点型)modbus上传
    vite+vue3+ts项目基础配置
    懵了?一夜之间,Rust 审核团队突然集体辞职
    做得好 vs 做得快?
  • 原文地址:https://blog.csdn.net/Java_zhujia/article/details/128182649