SpringBoot 整合多数据源的事务问题

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 多数据源实现、切面的优先级问题(Order注解)、事务的传播特性(Propagation类)

代码

先贴代码:
核心就是:Spring给我们提供的一个类 AbstractRoutingDataSource,然后我们再写一个切面来切换数据源,肯定要有一个地方存储key还要保证上下文都可用,所以我们使用 ThreadLocal 来存储数据源的key

pom.xml

<dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-aop</artifactId>
   </dependency>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
   
   <dependency>
       <groupId>com.alibaba</groupId>
       <artifactId>druid-spring-boot-starter</artifactId>
       <version>1.2.6</version>
   </dependency>
   <dependency>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
   </dependency>

注解:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DS {
    String value() default DataSourceCons.DS1;
}

常量类:

public interface DataSourceCons {
    String DS1 ="ds1";
    
    String DS2 ="ds2";
}

上下文存储对象:

@Slf4j
public class DynamicDataSourceContextHolder {
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    public static void setContextHolder(String dsName) {
        log.info("切换到 ===> {} 数据源", dsName);
        CONTEXT_HOLDER.set(dsName);
    }
    public static String get() {
        return CONTEXT_HOLDER.get();
    }
    public static void clear() {
        CONTEXT_HOLDER.remove();
    }
}

切面:后续这个Order注解有大用

@Aspect
@Component
// @Order(-1)
public class DynamicDataSourceAspect {
    @Pointcut("@annotation(com.lizhi.dds.anno.DS)")
    public void point1() {
    }
    @Around("point1()")
    public Object switchDS(ProceedingJoinPoint pjp) throws Throwable {
        try {
            DS ds = getDataSource(pjp);
            if (ds != null){
                DynamicDataSourceContextHolder.setContextHolder(ds.value());
            }
            return pjp.proceed();
        } finally {
            DynamicDataSourceContextHolder.clear();
        }
    }
    private DS getDataSource(ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        DS methodAnno = signature.getMethod().getAnnotation(DS.class);
        if (methodAnno != null) {
            return methodAnno;
        } else {
            return pjp.getTarget().getClass().getAnnotation(DS.class);
        }
    }
}

存储数据源的类:我们这里就写一个map来存储数据源,大家也可以加其它的属性

@Data
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.druid")
public class DruidProperties {
    private Map<String, Map<String, String>> ds;
}

核心类:我们写一个类来继承这个核心类来自定义我们自己的东西

@Component
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Autowired
    private DruidProperties druidProperties;
    /**
     * 初始化数据源
     *
     * @throws Exception
     */
    @PostConstruct
    public void init() throws Exception {
        Map<String, Map<String, String>> ds = druidProperties.getDs();
        Map<Object, Object> dataSources = new HashMap<>();
        for (Map.Entry<String, Map<String, String>> entry : ds.entrySet()) {
            DataSource dataSource = DruidDataSourceFactory.createDataSource(entry.getValue());
            dataSources.put(entry.getKey(), dataSource);
        }
        setTargetDataSources(dataSources);
        setDefaultTargetDataSource(dataSources.get(DataSourceCons.DS1));
        afterPropertiesSet();
    }
  
    /**
     * 拿数据源的时候会调用此方法来找key
     *
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.get();
    }
}

直接来看一下这个类的源码:就知道动态数据源是怎么来的了

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
  // 我们所有的数据源都会存储在这里面
  @Nullable
  private Map<Object, Object> targetDataSources;
  // 默认用哪个数据源
  @Nullable
  private Object defaultTargetDataSource;
  private boolean lenientFallback = true;
  private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
  // 解析过后的所有数据源
  @Nullable
  private Map<Object, DataSource> resolvedDataSources;
  // 解析过后的默认数据源
  @Nullable
  private DataSource resolvedDefaultDataSource;
  /**
   * Specify the map of target DataSources, with the lookup key as key.
   * The mapped value can either be a corresponding {@link javax.sql.DataSource}
   * instance or a data source name String (to be resolved via a
   * {@link #setDataSourceLookup DataSourceLookup}).
   * <p>The key can be of arbitrary type; this class implements the
   * generic lookup process only. The concrete key representation will
   * be handled by {@link #resolveSpecifiedLookupKey(Object)} and
   * {@link #determineCurrentLookupKey()}.
   */
  public void setTargetDataSources(Map<Object, Object> targetDataSources) {
    this.targetDataSources = targetDataSources;
  }
  /**
   * Specify the default target DataSource, if any.
   * <p>The mapped value can either be a corresponding {@link javax.sql.DataSource}
   * instance or a data source name String (to be resolved via a
   * {@link #setDataSourceLookup DataSourceLookup}).
   * <p>This DataSource will be used as target if none of the keyed
   * {@link #setTargetDataSources targetDataSources} match the
   * {@link #determineCurrentLookupKey()} current lookup key.
   */
  public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
    this.defaultTargetDataSource = defaultTargetDataSource;
  }
  @Override
  public void afterPropertiesSet() {
    if (this.targetDataSources == null) {
      throw new IllegalArgumentException("Property 'targetDataSources' is required");
    }
    this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
    this.targetDataSources.forEach((key, value) -> {
      Object lookupKey = resolveSpecifiedLookupKey(key);
      DataSource dataSource = resolveSpecifiedDataSource(value);
      this.resolvedDataSources.put(lookupKey, dataSource);
    });
    if (this.defaultTargetDataSource != null) {
      this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
    }
  }
  /**
   * Resolve the given lookup key object, as specified in the
   * {@link #setTargetDataSources targetDataSources} map, into
   * the actual lookup key to be used for matching with the
   * {@link #determineCurrentLookupKey() current lookup key}.
   * <p>The default implementation simply returns the given key as-is.
   * @param lookupKey the lookup key object as specified by the user
   * @return the lookup key as needed for matching
   */
  protected Object resolveSpecifiedLookupKey(Object lookupKey) {
    return lookupKey;
  }
  /**
   * Resolve the specified data source object into a DataSource instance.
   * <p>The default implementation handles DataSource instances and data source
   * names (to be resolved via a {@link #setDataSourceLookup DataSourceLookup}).
   * @param dataSource the data source value object as specified in the
   * {@link #setTargetDataSources targetDataSources} map
   * @return the resolved DataSource (never {@code null})
   * @throws IllegalArgumentException in case of an unsupported value type
   */
  protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
    if (dataSource instanceof DataSource) {
      return (DataSource) dataSource;
    }
    else if (dataSource instanceof String) {
      return this.dataSourceLookup.getDataSource((String) dataSource);
    }
    else {
      throw new IllegalArgumentException(
          "Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
    }
  }
  /**
   * Return the resolved default target DataSource, if any.
   * @return the default DataSource, or {@code null} if none or not resolved yet
   * @since 5.2.9
   * @see #setDefaultTargetDataSource
   */
  @Nullable
  public DataSource getResolvedDefaultDataSource() {
    return this.resolvedDefaultDataSource;
  }
  /**
   * 决定要使用哪个数据源的方法(核心)
   */
  protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    // 先拿到数据源的key
    Object lookupKey = determineCurrentLookupKey();
    // 从我们初始化好的map里根据key拿到一个数据源
    DataSource dataSource = this.resolvedDataSources.get(lookupKey);
    // 如果没拿到就用默认的数据源
    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
      dataSource = this.resolvedDefaultDataSource;
    }
    if (dataSource == null) {
      throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    }
    return dataSource;
  }
  /**
   * 找一下数据源的key(我们重写了此方法,所以拿到的就是我们上下文里存储的key)
   */
  @Nullable
  protected abstract Object determineCurrentLookupKey();
}

application.yml:

server:
  port: 9999
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      ds:
        ds1:
          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
          username: root
          password: root
          driverClassName: com.mysql.cj.jdbc.Driver
        ds2:
          url: jdbc:mysql://localhost:3306/atguigudb?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
          username: root
          password: root
          driverClassName: com.mysql.cj.jdbc.Driver

在org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineTargetDataSource 方法上打一个断点,然后发一个请求过去就能看到了,肯定一看就懂。


然后我们来说遇到的问题。

发现问题

问题1:两个库用同一个事务的问题

当我们使用如下代码时就会出现事务问题:

代码:

@Transactional
    @Override
    public List<SysUser> listAll() {
        System.out.println("employeeService.listAll() = " + employeeService.listAll());
        return sysUserMapper.selectList(null);
    }
    @DS(DataSourceCons.DS2)
    @Override
    public List<Employees> listAll() {
        return employeeMapper.selectList(null);
    }

问题截图:

我们发现的确出现了问题,发现第二张表的数据源使用的还是上一个的数据源,那我们就会想,是不是数据源切换没成功啊?别想了,我们看一下控制台就可以发现的确有打印(蓝色框里),说明走切面了。
那是哪的问题呢。

我们又会想到两个库使用同一个事务肯定会有问题。那我们就直接用Spring的事务的传播特性来解决不就好了,于是我们就直接上手改传播特性为 REQUIRES_NEW(默认的传播特性REQUIRED 也就是使用同一个事务,那我们就直接在第二个方法上加上事务注解并设置传播特性REQUIRES_NEW 即可,就是创建一个新事务,使用不同的事务)。我们改完之后发现还是没什么卵用。那我们就会疑惑了那是哪的问题呢?

解决方案:事务传播特性设置为 REQUIRES_NEW

问题2:优先级问题

这个问题的答案我就直接说了:事务的原理是代理,我们切换数据源的切面的原理也是代理,它俩的执行的前后顺序是有问题的。我们需要把切换数据源的切面让它在事务前执行即可。

解决方案:也就是需要加个Order注解来提升优先级。

源码分析

第一次Debug

先在第二个事务的方法设置传播特性为 REQUIRES_NEW,然后来分析。最后会分析 REQUIRED 为啥不行

我们直接在事务的方法上打一个断点:

那我们就会想了,都不知道从哪看,那该怎么办,没关系:不知道从哪看很简单,我们直接看调用栈来找。

我们通过调用栈可以发现:

  • 红框里是我们自己的方法,所以不用管
  • 紫框里是代理,所以也不用管
  • 蓝框里我们一看有 Transaction 关键字,那不就是事务相关的吗,好家伙,这不就找到了

那我们直接就在上面打个断点org.springframework.transaction.interceptor.TransactionInterceptor#invoke
发现它调用了 org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction 方法

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
      final InvocationCallback invocation) throws Throwable {
    // If the transaction attribute is null, the method is non-transactional.
    // 如果transaction属性为null,则该方法为非事务性方法。
    TransactionAttributeSource tas = getTransactionAttributeSource();
    final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
    if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
      // 如果需要的话就创建一个事务(**核心**)
      TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
      Object retVal;
      try {
        // 执行目标方法
        retVal = invocation.proceedWithInvocation();
      }
      catch (Throwable ex) {
        // target invocation exception
        // 出现异常就处理异常
        completeTransactionAfterThrowing(txInfo, ex);
        throw ex;
      }
      finally {
        cleanupTransactionInfo(txInfo);
      }
      if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
        // Set rollback-only in case of Vavr failure matching our rollback rules...
        TransactionStatus status = txInfo.getTransactionStatus();
        if (status != null && txAttr != null) {
          retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
        }
      }
      
      // 提交事务
      commitTransactionAfterReturning(txInfo);
      return retVal;
    }
  }

前面的都不需要看,直接看 createTransactionIfNecessary() 方法:TransactionAspectSupport类

protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
      @Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
    TransactionStatus status = null;
    if (txAttr != null) {
      if (tm != null) {
        // 获取一个事务(核心)
        status = tm.getTransaction(txAttr);
      }
      else {
        if (logger.isDebugEnabled()) {
          logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
              "] because no transaction manager has been configured");
        }
      }
    }
    return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
  }

来到真正的核心 getTransaction() 方法:AbstractPlatformTransactionManager类

public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
      throws TransactionException {
    // 事务的定义信息
    TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());
    // 获取一个事务
    Object transaction = doGetTransaction();
    boolean debugEnabled = logger.isDebugEnabled();
    // 已经有事务的处理
    if (isExistingTransaction(transaction)) {
      // Existing transaction found -> check propagation behavior to find out how to behave.
      return handleExistingTransaction(def, transaction, debugEnabled);
    }
    // 没有事务的处理
    if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
      throw new IllegalTransactionStateException(
          "No existing transaction found for transaction marked with propagation 'mandatory'");
    }
    else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
        def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
        def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
      SuspendedResourcesHolder suspendedResources = suspend(null);
      try {
        // 开启一个事务
        return startTransaction(def, transaction, debugEnabled, suspendedResources);
      }
    }
  }

三个方法:doGetTransaction() 方法、handleExistingTransaction() 方法、startTransaction() 方法。我们一个一个来看。

第一个方法:

org.springframework.jdbc.datasource.DataSourceTransactionManager#doGetTransaction

protected Object doGetTransaction() {
    DataSourceTransactionObject txObject = new DataSourceTransactionObject();
    txObject.setSavepointAllowed(isNestedTransactionAllowed());
    ConnectionHolder conHolder =
        (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());
    txObject.setConnectionHolder(conHolder, false);
    return txObject;
  }

这是第一次这三个的值。

核心呢其实就是从缓存里获取了ConnectionHolder对象,连接缓存

来看一下这个方法:

org.springframework.transaction.support.TransactionSynchronizationManager#getResource

private static final ThreadLocal<Map<Object, Object>> resources =
      new NamedThreadLocal<>("Transactional resources");
  public static Object getResource(Object key) {
    Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
    return doGetResource(actualKey);
  }
  /**
   * Actually check the value of the resource that is bound for the given key.
   */
  @Nullable
  private static Object doGetResource(Object actualKey) {
    Map<Object, Object> map = resources.get();
    if (map == null) {
      return null;
    }
    Object value = map.get(actualKey);
    // Transparently remove ResourceHolder that was marked as void...
    if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
      map.remove(actualKey);
      // Remove entire ThreadLocal if empty...
      if (map.isEmpty()) {
        resources.remove();
      }
      value = null;
    }
    return value;
  }

我们不是继承了AbstractRoutingDataSource这个类吗,这个key呢就是我们的这个类。

第一次肯定是拿不到连接缓存的,所以一路返回。又到了 getTransaction() 这个大方法

第一次肯定也是不存在事务的,所以也不会走我们的 第二个核心方法 handleExistingTransaction()

所以就来到了我们的第三个核心方法

org.springframework.transaction.support.AbstractPlatformTransactionManager#startTransaction

private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction,
      boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) {
    boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
    DefaultTransactionStatus status = newTransactionStatus(
        definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
    // 开始
    doBegin(transaction, definition);
    prepareSynchronization(status, definition);
    return status;
  }

直接看 doBegin() 方法
org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin

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");
        }
        // 给当前事务设置一个连接缓存
        txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
      }
      // 设置一堆事务的特性:隔离级别啊、自动提交啊什么的
      txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
      con = txObject.getConnectionHolder().getConnection();
      Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
      txObject.setPreviousIsolationLevel(previousIsolationLevel);
      txObject.setReadOnly(definition.isReadOnly());
      // Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
      // so we don't want to do it unnecessarily (for example if we've explicitly
      // configured the connection pool to set it already).
      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);
      }
      if (txObject.isNewConnectionHolder()) {
        // 给当前线程绑定一下连接缓存,后续再来方便获取
        // 也就是往刚才的那个map里放一下
        // key还是我们的那个数据源的类,value就是连接缓存对象
        // 这个也是导致我们的问题所在处二
        TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
      }
    }
  }

总结:就是根据我们的数据源来获取一个连接,并缓存起来,以供后续使用。并会设置一些事务的相关信息。

我们当前获取到的连接:

绑定的缓存:key还是我们的那个类,value就是我们根据当前数据源获取到的连接,当前数据源是第一个数据源。我们
接下来看一下第二个数据源的连接是什么。

我们来看一下 obtainDataSource().getConnection() 这个方法是怎么做的:AbstractRoutingDataSource类

@Override
  public Connection getConnection() throws SQLException {
    return determineTargetDataSource().getConnection();
  }
  
  protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    Object lookupKey = determineCurrentLookupKey();
    DataSource dataSource = this.resolvedDataSources.get(lookupKey);
    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
      dataSource = this.resolvedDefaultDataSource;
    }
    if (dataSource == null) {
      throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    }
    return dataSource;
  }
  
  // 我们写的实现类
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.get();
    }

是不是很熟悉?这不就是Spring给我们提供的来做动态数据源那个类的源码吗!前面已经分析过了

由于我们上下文存储的key是null,所以就给了一个默认数据源。所以获取到的是第一个数据源的连接。我们看一下第二个方法是否还是这个数据源,让我们进入第二次 Debug吧。

第二次Debug

前面都是一样的套路,但是走到这里就会发生变化了。还记得我们的三个核心方法吗?第一次 Debug的时候并没有走我们的第二个核心方法,这一次就要走了

进来的条件:判断当前事务对象是否有连接缓存并且当前连接缓存的事务是活跃的

org.springframework.transaction.support.AbstractPlatformTransactionManager#handleExistingTransaction:

private TransactionStatus handleExistingTransaction(
      TransactionDefinition definition, Object transaction, boolean debugEnabled)
      throws TransactionException {
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {
      throw new IllegalTransactionStateException(
          "Existing transaction found for transaction marked with propagation 'never'");
    }
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
      if (debugEnabled) {
        logger.debug("Suspending current transaction");
      }
      Object suspendedResources = suspend(transaction);
      boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
      return prepareTransactionStatus(
          definition, null, false, newSynchronization, debugEnabled, suspendedResources);
    }
    // 我们设置的传播特性会走这里
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
      if (debugEnabled) {
        logger.debug("Suspending current transaction, creating new transaction with name [" +
            definition.getName() + "]");
      }
      // 清除一下上一次事务的信息
      SuspendedResourcesHolder suspendedResources = suspend(transaction);
      try {
        // 开启一个新的事务(核心)
        return startTransaction(definition, transaction, debugEnabled, suspendedResources);
      }
      catch (RuntimeException | Error beginEx) {
        resumeAfterBeginException(transaction, suspendedResources, beginEx);
        throw beginEx;
      }
    }
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
      if (!isNestedTransactionAllowed()) {
        throw new NestedTransactionNotSupportedException(
            "Transaction manager does not allow nested transactions by default - " +
            "specify 'nestedTransactionAllowed' property with value 'true'");
      }
      if (debugEnabled) {
        logger.debug("Creating nested transaction with name [" + definition.getName() + "]");
      }
      if (useSavepointForNestedTransaction()) {
        // Create savepoint within existing Spring-managed transaction,
        // through the SavepointManager API implemented by TransactionStatus.
        // Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization.
        DefaultTransactionStatus status =
            prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);
        status.createAndHoldSavepoint();
        return status;
      }
      else {
        // Nested transaction through nested begin and commit/rollback calls.
        // Usually only for JTA: Spring synchronization might get activated here
        // in case of a pre-existing JTA transaction.
        return startTransaction(definition, transaction, debugEnabled, null);
      }
    }
    // Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED.
    if (debugEnabled) {
      logger.debug("Participating in existing transaction");
    }
    if (isValidateExistingTransaction()) {
      if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
        Integer currentIsolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel();
        if (currentIsolationLevel == null || currentIsolationLevel != definition.getIsolationLevel()) {
          Constants isoConstants = DefaultTransactionDefinition.constants;
          throw new IllegalTransactionStateException("Participating transaction with definition [" +
              definition + "] specifies isolation level which is incompatible with existing transaction: " +
              (currentIsolationLevel != null ?
                  isoConstants.toCode(currentIsolationLevel, DefaultTransactionDefinition.PREFIX_ISOLATION) :
                  "(unknown)"));
        }
      }
      if (!definition.isReadOnly()) {
        if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
          throw new IllegalTransactionStateException("Participating transaction with definition [" +
              definition + "] is not marked as read-only but existing transaction is");
        }
      }
    }
    // 默认的传播特性走这里
    boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
    return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
  }

总结:就是根据不同的传播特性来做不同的事情。

先来看一下一个重要的事情,我们发现进来这个方法之后 Transaction 对象里还是有之前的连接缓存的,这是万万不行的,所以需要清掉。

当我们经过这个方法 suspend() 后,我们神奇的发现里面的信息没有了:

直接来看一下这个方法:

protected final SuspendedResourcesHolder suspend(@Nullable Object transaction) throws TransactionException {
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
      List<TransactionSynchronization> suspendedSynchronizations = doSuspendSynchronization();
      try {
        Object suspendedResources = null;
        if (transaction != null) {
          // 清除连接信息
          suspendedResources = doSuspend(transaction);
        }
        String name = TransactionSynchronizationManager.getCurrentTransactionName();
        TransactionSynchronizationManager.setCurrentTransactionName(null);
        boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
        TransactionSynchronizationManager.setCurrentTransactionReadOnly(false);
        Integer isolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel();
        TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(null);
        boolean wasActive = TransactionSynchronizationManager.isActualTransactionActive();
        TransactionSynchronizationManager.setActualTransactionActive(false);
        return new SuspendedResourcesHolder(
            suspendedResources, suspendedSynchronizations, name, readOnly, isolationLevel, wasActive);
      }
      catch (RuntimeException | Error ex) {
        // doSuspend failed - original transaction is still active...
        doResumeSynchronization(suspendedSynchronizations);
        throw ex;
      }
    }
    else if (transaction != null) {
      // Transaction active but no synchronization active.
      Object suspendedResources = doSuspend(transaction);
      return new SuspendedResourcesHolder(suspendedResources);
    }
    else {
      // Neither transaction nor synchronization active.
      return null;
    }
  }

它里面又调用了这个方法来做:

org.springframework.jdbc.datasource.DataSourceTransactionManager#doSuspend

@Override
  protected Object doSuspend(Object transaction) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
    // 设置为null
    txObject.setConnectionHolder(null);
    return TransactionSynchronizationManager.unbindResource(obtainDataSource());
  }

然后我们直接来看我们的传播特性做的事情。其实还是之前 startTransaction() 做的事情。就是开
启一个新的事务来做。我们可以看一下这一次的连接是谁的

我们发现这次的连接还是第一个数据源的连接,这就出大问题了。我们想要的是第二个数据源的连接。这是怎么回事呢?不要急,我们直接来看。

这是咋回事啊,我靠,切面没成功吗,key还是null,所以获取到的数据源就是默认的数据源了。来看一下控制台是否打印切换数据源的信息:确实没有信息

当我们全部放行之后,再次查看控制台就会发现,我们的数据源切换成功了!

what?这是什么情况。这个时候我们就要运用大脑里的知识来想一想了,想到原理。来回 Debug 之后,我有了个想法:Spring事务是aop,我们的这个切换数据源的也是aop,那会不会是切面之间的执行顺序还有先后啊。

于是乎我们直接实践,直接在切换数据源的切面上加上了 Order 注解,来提高优先级。并在获取数据源key的地方和切换数据源的切面上这两个地方打了断点,我们会发现,的确是有优先级的。加了之后,我们神奇的发现成功了!Amazing!

解决

优先级:

再次一路 Debug 到doBegin() 方法来验证猜想:

打开控制台查看,也会发现有信息输出:

直接成功!

同一个事务问题:

我们把第二个方法的事务的传播特性还设置回原来的 REQUIRED 或者不加事务注解。

还是来到第二个核心方法:handleExistingTransaction()

还记得我之前在此处代码上加的注释吗?如果是默认的传播特性会走这里来处理

private TransactionStatus handleExistingTransaction(
      TransactionDefinition definition, Object transaction, boolean debugEnabled)
      throws TransactionException {
                 ......
    // Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED.
    // 处理 PROPAGATION_SUPPORTS 和 PROPAGATION_REQUIRED 的传播特性
    boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
    return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
  }

prepareTransactionStatus():

protected final DefaultTransactionStatus prepareTransactionStatus(
      TransactionDefinition definition, @Nullable Object transaction, boolean newTransaction,
      boolean newSynchronization, boolean debug, @Nullable Object suspendedResources) {
    DefaultTransactionStatus status = newTransactionStatus(
        definition, transaction, newTransaction, newSynchronization, debug, suspendedResources);
    prepareSynchronization(status, definition);
    return status;
  }

就是把当前的事务状态给返回了,所以后续拿到的连接还是上一个的

总结

  1. 切面的优先级问题(Order注解)
  2. 事务的传播特性(Propagation类)

文章到这里就结束了,应该还是有点长的。希望有点帮助哈。

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
19天前
|
前端开发 Java 数据库连接
Spring Boot 3 整合 Mybatis-Plus 动态数据源实现多数据源切换
Spring Boot 3 整合 Mybatis-Plus 动态数据源实现多数据源切换
|
19天前
|
Java 关系型数据库 数据库
Spring Boot多数据源及事务管理:概念与实战
【4月更文挑战第29天】在复杂的企业级应用中,经常需要访问和管理多个数据源。Spring Boot通过灵活的配置和强大的框架支持,可以轻松实现多数据源的整合及事务管理。本篇博客将探讨如何在Spring Boot中配置多数据源,并详细介绍事务管理的策略和实践。
51 3
|
19天前
|
存储 关系型数据库 MySQL
【mybatis-plus】Springboot+AOP+自定义注解实现多数据源操作(数据源信息存在数据库)
【mybatis-plus】Springboot+AOP+自定义注解实现多数据源操作(数据源信息存在数据库)
|
19天前
|
druid Java 数据库连接
SpringBoot + Mybatis + Druid + PageHelper 实现多数据源分页
SpringBoot + Mybatis + Druid + PageHelper 实现多数据源分页
53 0
|
19天前
|
Java 数据库连接 数据库
Spring Boot整合MyBatis Plus集成多数据源轻松实现数据读写分离
Spring Boot整合MyBatis Plus集成多数据源轻松实现数据读写分离
43 2
|
19天前
|
存储 Java 关系型数据库
springboot整合多数据源的配置以及动态切换数据源,注解切换数据源
springboot整合多数据源的配置以及动态切换数据源,注解切换数据源
114 0
QGS
|
19天前
|
Java 关系型数据库 MySQL
手拉手springboot3整合mybatis-plus多数据源
手拉手springboot3整合mybatis-plus多数据源
QGS
111 1
|
19天前
|
Java 数据库 数据安全/隐私保护
使用Spring Boot和JPA实现多数据源的方法
使用Spring Boot和JPA实现多数据源的方法
84 0
|
19天前
|
Java 数据库连接 数据库
【Spring技术专题】「实战开发系列」保姆级教你SpringBoot整合Mybatis框架实现多数据源的静态数据源和动态数据源配置落地
Mybatis是一个基于JDBC实现的,支持普通 SQL 查询、存储过程和高级映射的优秀持久层框架,去掉了几乎所有的 JDBC 代码和参数的手工设置以及对结果集的检索封装。 Mybatis主要思想是将程序中大量的 SQL 语句剥离出来,配置在配置文件中,以实现 SQL 的灵活配置。在所有 ORM 框架中都有一个非常重要的媒介——PO(持久化对象),PO 的作用就是完成持久化操作,通过该对象对数据库执行增删改的操作,以面向对象的方式操作数据库。
58 1
【Spring技术专题】「实战开发系列」保姆级教你SpringBoot整合Mybatis框架实现多数据源的静态数据源和动态数据源配置落地
|
19天前
|
Java 数据库
Springboot 之 Mybatis-plus 多数据源
Springboot 之 Mybatis-plus 多数据源
52 0