• 自己动手写一个分库分表中间件(九)兼容性处理之事务之 Spring 怎么看是一个事务


    欢迎关注微信公众号:冬瓜白

    相关文章:

    兼容是本次分库分表项目中非常重要的一点,本文探讨的议题是兼容中的事务问题。

    实在是不知道这个标题名称该怎么取。直接看一个例子吧,这也是是很多朋友经常有疑问的一个点。

    //srvService1Mapper和srvService2Mapper分别配置了不同的数据源、事务管理器,srvService1Mapper的事务管理器标注了@Primary
    @Transactional
    public void bbb(String sId1,String sId2) {
        srvService1Mapper.updateByid(sId1);   
        srvService2Mapper.updateByid(sId2);   
        int i = 1/0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这个方法非常简单,一个被 @Transactional 标注的方法,内部调用了两个配置了不同数据源和事务管理器的 Mapper ,要注意的是,这里的 @Transactional 并未设置任何参数,即属性参数都用默认的。

    那么这个方法在执行后,是 srvService1Mapper 会回滚还是 srvService2Mapper 会回滚呢?

    @Transactional 会使用哪个事务管理器?

    先看第一个问题:@Transactional 会使用哪个事务管理器?很明显,如果指定了事务管理器的 BeanName,那么肯定就是对应的事务管理器。如果没有指定的话就会使用默认事务管理器,那么默认事务管理器是什么呢,可以看下这个方法:

    org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction->org.springframework.transaction.interceptor.TransactionAspectSupport#determineTransactionManager

    	protected PlatformTransactionManager determineTransactionManager(TransactionAttribute txAttr) {
    		// Do not attempt to lookup tx manager if no tx attributes are set
    		if (txAttr == null || this.beanFactory == null) {
    			return getTransactionManager();
    		}
    		String qualifier = txAttr.getQualifier();
    		if (StringUtils.hasText(qualifier)) {
    			return determineQualifiedTransactionManager(qualifier);
    		}
    		else if (StringUtils.hasText(this.transactionManagerBeanName)) {
    			return determineQualifiedTransactionManager(this.transactionManagerBeanName);
    		}
    		else {
    			PlatformTransactionManager defaultTransactionManager = getTransactionManager();
    			if (defaultTransactionManager == null) {
    				defaultTransactionManager = this.transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY);
    				if (defaultTransactionManager == null) {
    					defaultTransactionManager = this.beanFactory.getBean(PlatformTransactionManager.class);
    					this.transactionManagerCache.putIfAbsent(
    							DEFAULT_TRANSACTION_MANAGER_KEY, defaultTransactionManager);
    				}
    			}
    			return defaultTransactionManager;
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    这个方法说白了就是先解析 @Transactional 的属性,解析不出来就走默认的,这里的默认其实就是从 Bean 容器中找到就是被 @Primary 标注的事务管理器,这个就看我们是怎么配置的,如果没有配置被 @Primary 标注的事务管理器,那么就会报错:

    No qualifying bean of type 'org.springframework.transaction.PlatformTransactionManager' available: expected single matching bean but found 7:
    
    • 1

    上面也提到了,srvService1Mapper 的事务管理器标注了 @Primary

    这个问题确定了,那么再看第二个问题。

    怎么看是同一个事务

    我们都知道,Spring 的声明式事务是通过 @Transactional 来实现的,现在已经确定了,这里的 @Transactional 使用的是 srvService1Mapper 的事务管理器。

    这里“同一个事务”的指的是, @Transactional 的事务管理器和被 @Transactional 标注方法内部的 Mapper 使用的事务是否是同一个。在这个例子中,很明显 srvService1Mapper@Transactional 是同一个。

    事务的本质是什么

    在“《从单机事务到分布式事务》分享文档”中提到过,事务是 Connection 级别的,Connection 从哪里来?从 DataSource 里面来。这里有个要注意的是,MapperSqlSessionFactory 要配置 DataSource,同时事务管理器也是要配置 DataSource的:

    org.mybatis.spring.SqlSessionFactoryBean#setDataSource

      public void setDataSource(DataSource dataSource) {
        if (dataSource instanceof TransactionAwareDataSourceProxy) {
          // If we got a TransactionAwareDataSourceProxy, we need to perform
          // transactions for its underlying target DataSource, else data
          // access code won't see properly exposed transactions (i.e.
          // transactions for the target DataSource).
          this.dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource();
        } else {
          this.dataSource = dataSource;
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    org.springframework.jdbc.datasource.DataSourceTransactionManager#DataSourceTransactionManager(javax.sql.DataSource)

    	public DataSourceTransactionManager(DataSource dataSource) {
    		this();
    		setDataSource(dataSource);
    		afterPropertiesSet();
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    @Transactional 从哪里获取 Connection

    @Transactional 开启的时候一开始就会去获取 Connection,即从 @Transactional 配置的事务管理器中配置的 DataSource 中获取 Connection

    srvService1Mapper 从哪里获取 Connection

    很明显,从配置的 SqlSessionFactory 中配置的 DataSource 中获取 ConnectionsrvService2Mapper同理。

    MapperConnection 怎么和 @Transactional 结合起来

    这里有个注意点,就是 Spring 与不同的数据库访问框架,实现细节会略有不同,但总体流程其实差不多。

    上面已经提到过,事务是 Connection 级别,也就是说这个 Mapper 的事务要想生效,必须和 @TransactionalConnection 是同一个。

    流程实在太复杂,直接看最关键的:

    org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource

      private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        try {
          final Environment environment = configuration.getEnvironment();
          final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
          tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
          final Executor executor = configuration.newExecutor(tx, execType);
          return new DefaultSqlSession(configuration, executor, autoCommit);
        } catch (Exception e) {
          closeTransaction(tx); // may have fetched a connection so lets call close()
          throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    可以看到是从 MyBatis 的 Configuration 获取的 DataSource,再看 org.springframework.jdbc.datasource.DataSourceUtils#doGetConnection

    public static Connection doGetConnection(DataSource dataSource) throws SQLException {
    		Assert.notNull(dataSource, "No DataSource specified");
    
    		ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
      ....
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可以看到是从 TransactionSynchronizationManager 中获取 Connection,即 ThreadLocal,简单点说就是从一个内存 Map 中获取 Connection,key 是 DataSource

    但这里是不是就很通透了,即只要 DataSource 是一致的,那么 Connection 就是一致的(当然这块是可以重写的,具体可以参看自己动手写一个分库分表中间件(七)分布式事务问题解决思路<三>动态 Connection),那么就可以被 @Transactional 所管理。

    再回过头看文章最开头的例子,很明显,只有 srvService1Mapper 的事务会回滚,因为 srvService2Mapper 的事务已经不受 @Transactional 管理了。

    这里可能还有朋友有疑问,不受 @Transactional 管理了,那么这个事务怎么提交的呢,其实事务只要没有设置非自动提交,那么就会自动提交了。

    本文比较详细的分析了 DataSource 在事务管理中的重要性,而这一点也是设计分库分表中间件中兼容历史场景的重要原理之一。

    References

    • https://mp.weixin.qq.com/s?__biz=MzU1OTgyMDc3Mg==&mid=2247485239&idx=1&sn=e2e54f7ad284e4266309953a199789fd&chksm=fc103dbccb67b4aa91737765caacea66574975600c9636fda527561978f70993c83fc68d3018&token=405931768&lang=zh_CN#rd
  • 相关阅读:
    第十一届中国云计算标准和应用大会 | 云计算国家标准及白皮书系列发布 华云数据全面参与编制
    Kubernetes 系统化学习之 基本概念篇
    大型连锁百货运维审计用什么软件好?有哪些功能?
    阿里p8软测专家耗时一个月整理出,从0基础自学到功能测试再到自动化测试超全学习指南
    JZ11 旋转数组的最小数字
    Linux 之 vim
    6.Flink实时项目之业务数据分流
    Smartbi入选Gartner ABI领域“客户之声”,荣获“卓越表现者”
    199、在RabbitMQ管理控制台中管理 Exchange(充当消息交换机的组件) 和 Queue(消息队列),以及对默认Exchange的讲解
    JavaScript面向对象学习构造函数、静态成员和实例成员(二)
  • 原文地址:https://blog.csdn.net/Dongguabai/article/details/126341032