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

代码

先贴代码:核心就是: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 {@Autowiredprivate DruidProperties druidProperties;/*** 初始化数据源** @throws Exception*/@PostConstructpublic 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*/@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceContextHolder.get();}
}

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

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {// 我们所有的数据源都会存储在这里面@Nullableprivate Map<Object, Object> targetDataSources;// 默认用哪个数据源@Nullableprivate Object defaultTargetDataSource;private boolean lenientFallback = true;private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();// 解析过后的所有数据源@Nullableprivate Map<Object, DataSource> resolvedDataSources;// 解析过后的默认数据源@Nullableprivate 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;}@Overridepublic 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*/@Nullablepublic DataSource getResolvedDefaultDataSource() {return this.resolvedDefaultDataSource;}/*** 决定要使用哪个数据源的方法(核心)*/protected DataSource determineTargetDataSource() {Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");// 先拿到数据源的keyObject 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)*/@Nullableprotected abstract Object determineCurrentLookupKey();}

application.yml:

server:port: 9999spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedruid:ds:ds1:url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8username: rootpassword: rootdriverClassName: com.mysql.cj.jdbc.Driverds2:url: jdbc:mysql://localhost:3306/atguigudb?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8username: rootpassword: rootdriverClassName: com.mysql.cj.jdbc.Driver

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


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

发现问题

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

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

代码:

    @Transactional@Overridepublic List<SysUser> listAll() {System.out.println("employeeService.listAll() = " + employeeService.listAll());return sysUserMapper.selectList(null);}@DS(DataSourceCons.DS2)@Overridepublic 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.*/@Nullableprivate 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类

	@Overridepublic 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;}// 我们写的实现类@Overrideprotected 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

	@Overrideprotected Object doSuspend(Object transaction) {DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;// 设置为nulltxObject.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类)

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

文章转载自:zxyyu

原文链接:https://www.cnblogs.com/wmyxy/p/17999143

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/660238.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

结合创新!11种多尺度特征融合方法,附论文和代码

随着深度学习和计算机视觉技术的快速发展&#xff0c;多尺度特征融合已经成为一个备受关注的、不断探索的研究方向&#xff0c;它通过捕捉不同尺度和层次上的特征信息&#xff0c;提高对图像和视频内容的理解能力&#xff0c;为图像处理、计算机视觉和深度学习等领域的应用提供…

鸿蒙(HarmonyOS)项目方舟框架(ArkUI)之DatePicker组件

鸿蒙&#xff08;HarmonyOS&#xff09;项目方舟框架&#xff08;ArkUI&#xff09;之DatePicker组件 一、操作环境 操作系统: Windows 10 专业版、IDE:DevEco Studio 3.1、SDK:HarmonyOS 3.1 二、DatePicker组件 日期选择器组件&#xff0c;用于根据指定日期范围创建日期滑…

字符串操作函数1

1.strcpy使用 使用这个函数我们可以进行字符串拷贝。它有两个参数&#xff0c;第一个参数是指向目标空间&#xff0c;第二个参数是指向需要拷贝的字符串。返回值为拷贝完成后指向的字符串首地址。头文件为<string.h> 演示如下&#xff1a; 注意&#xff1a; • 源字符…

TensorFlow2实战-系列教程4:数据增强

&#x1f9e1;&#x1f49b;&#x1f49a;TensorFlow2实战-系列教程 总目录 有任何问题欢迎在下面留言 本篇文章的代码运行界面均在Jupyter Notebook中进行 本篇文章配套的代码资源已经上传 猫狗识别1 数据增强 猫狗识别2------数据增强 猫狗识别3------迁移学习 对于图像数据…

RS485自动收发电路震荡的问题

电路 设计初衷 电源5V 选择5V的原因&#xff0c;差分2.5V比1.5V可以提高传输能力 TTL输入 3.3V电平满足需求 TTL输出 4.5V了&#xff0c;MCU是3.3V平台 这样就分为两种情况 MCU接收端可以容忍5V输入 MCU接收端不可以容忍5V输入&#xff0c;就要进行电压转换&#xff0c;我这里使…

MacOS X 中 OpenGL 环境搭建 Makefile的方式

1&#xff0c;预备环境 安装 brew&#xff1a; /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 安装glfw&#xff1a; brew install glfw 安装glew&#xff1a; brew install glew 2.编译 下载源代码…

本地搭建Plex私人影音网站并结合内网穿透实现公网远程访问

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

【数据库】mysql触发器使用

题目&#xff1a; 创建职工表以及职工工资表职工表字段&#xff1a;工号&#xff0c;姓名&#xff0c;性别&#xff0c;年龄工资表字段&#xff1a;编号自增&#xff0c;职工工号&#xff0c;基础工资10000通过触发器实现&#xff1a;对职工进行添加时 工资表中也要体现当前职…

docker下,容器无法启动,要删除里面的文件

第一步&#xff1a;进入docker cd /var/lib/docker 第二步&#xff1a;查找&#xff0c;我这里是拼音分词器 find ./ -name py 第三步&#xff1a;得到路径 第四步&#xff1a;删除或复制或移动&#xff0c;我这里是删除py文件夹 rm -rf ./over那一串 第五步&#xff1a;想干…

D2025——双通道音频功率放大电路,外接元件少, 通道分离性好,3V 的低压下可正常使用

D2025 为立体声音频功率放大集成电路&#xff0c;适用于各类袖珍或便携式立体声 收录机中作功率放放大器。 D2025 采用 DIP16 封装形式。 主要特点&#xff1a;  适用于立体声或 BTL 工作模式  外接元件少  通道分离性好  电源电压范围宽&#xff08;3V~12V…

【JavaEE spring】SpringBoot 统一功能处理

SpringBoot 统一功能处理 1. 拦截器1.1 拦截器快速⼊⻔1.2 拦截器详解1.2.1 拦截路径1.2.2 拦截器执⾏流程 1.3 登录校验1.3.1 定义拦截器1.3.2 注册配置拦截器 2. 统⼀数据返回格式2.1 快速⼊⻔2.2 存在问题2.3 案例代码修改2.4 优点 3. 统⼀异常处理 1. 拦截器 后端程序根据…

Chakra UI:构建 Web 设计的未来

Chakra UI&#xff1a;构建 Web 设计的未来 在当今的Web开发领域&#xff0c;构建现代、可访问的用户界面是一个重要的任务。为了满足这一需求&#xff0c;开发者需要一个强大而易用的UI组件库。而Chakra UI作为一个基于React的开源组件库&#xff0c;正是为了解决这个问题而诞…

vue3 [Vue warn]: Unhandled error during execution of scheduler flush

文章目录 前言一、报错截图二、排除问题思路相关问题 Vue3 优雅解决方法异步组件异同之处&#xff1a;好处&#xff1a;在使用异步组件时&#xff0c;有几个注意点&#xff1a; vue3 定义与使用异步组件 总结 前言 Bug 记录。开发环境运行正常&#xff0c;构建后时不时触发下面…

hal库stm32串口接收不定长数据

参考博客&#xff1a; https://blog.csdn.net/qq_41830158/article/details/121254705 按下面步骤修改实测可用 步骤&#xff1a; 添加串口接收所需变量   打开uart.c文件&#xff0c;在文件顶部的USER CODE BEGIN 0下方添加下列变量 volatile uint8_t rx1_len 0; //接收…

C语言第十五弹---操作符(上)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】 操作符 1、操作符的分类 2、二进制和进制转换 2.1、2进制转10进制 2.1.1、10进制转2进制数字 2.2、2进制转8进制和16进制 2.2.2、2进制转16进制 3. 原码、反…

C++完成使用map Update数据 二进制数据

1、在LXMysql.h和LXMysql.cpp分别定义和编写关于pin语句的代码 //获取更新数据的sql语句 where语句中用户要包含where 更新std::string GetUpdatesql(XDATA kv, std::string table, std::string where); std::string LXMysql::GetUpdatesql(XDATA kv, std::string table, std…

智能小车案例:基于Raspberry Pi的自动巡航与避障系统

项目背景 随着物联网技术的不断发展&#xff0c;智能小车成为了现代生活和工业自动化中的重要工具。为了实现智能小车的自动巡航与避障功能&#xff0c;我们采用了Raspberry Pi作为主控制器&#xff0c;结合传感器和执行器&#xff0c;构建了一个完整的系统。 所需材料 Raspber…

幻兽帕鲁:10秒开服,一键配置游戏参数教程!

随着游戏行业的不断发展&#xff0c;玩家们对于游戏体验的要求也越来越高。为了满足玩家们的需求&#xff0c;腾讯云提供了游戏联机服务器一键部署方案&#xff0c;本文将为大家详细介绍如何基于腾讯云服务器10秒钟完成开服和配置游戏参数&#xff0c;让大家的游戏体验更加顺畅…

服装产业转型升级,iPayLinks帮助企业拓展市场盈更多

从十万件的大订单转变为几百件的小订单&#xff0c;小单快反模式为中国服装出口带来了机遇&#xff0c;也带来了挑战。 “十三行-中大-鹭江”是广州曾经最具代表性的外贸服装产业带。在过去很长的一段时间里&#xff0c;服装外贸老板在这里创造“神话”&#xff1a;24小时内完…

spdk技术原理简介和实践经验

一、导读 与机械硬盘相比&#xff0c;NVMe-ssd在性能、功耗和密度上都有巨大的优势&#xff0c;并且随着固态存储介质的高速发展&#xff0c;其价格也在大幅下降&#xff0c;这些优势使得NVMe-ssd在分布式存储中使用越来越广泛。由于NVMe-ssd的性能比传统磁盘介质高出很多&…