jpa+AbstractRoutingDataSource+Transactional数据源切换失效

事务开启过程

当一个请求内进行了多个dao层操作,且使用了Transactional事务,除了第一次会通过AbstractRoutingDataSource的getConnection方法切换数据源。后续CRUD操作,会包含在上一个事务中。不会进行数据源切换。
通过跟踪事务拦截链为:

方法
TransactionAspectSupportcreateTransactionIfNecessary
AbstractPlatformTransactionManagergetTransaction
JpaTransactionManagerdoGetTransaction
JpaTransactionManagerdoBegin
HibernateJpaDialectbeginTransaction
LogicalConnectionManagedImplacquireConnectionIfNeeded
DatasourceConnectionProviderImplgetConnection

问题原因在于以下三个方法

方法
AbstractPlatformTransactionManagergetTransaction
JpaTransactionManagerdoGetTransaction
LogicalConnectionManagedImplacquireConnectionIfNeeded
    public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
        Object transaction = this.doGetTransaction();
        boolean debugEnabled = this.logger.isDebugEnabled();
        if (definition == null) {
            definition = new DefaultTransactionDefinition();
        }

        if (this.isExistingTransaction(transaction)) {
            return this.handleExistingTransaction((TransactionDefinition)definition, transaction, debugEnabled);
        } else if (((TransactionDefinition)definition).getTimeout() < -1) {
            throw new InvalidTimeoutException("Invalid transaction timeout", ((TransactionDefinition)definition).getTimeout());
        } else if (((TransactionDefinition)definition).getPropagationBehavior() == 2) {
            throw new IllegalTransactionStateException("No existing transaction found for transaction marked with propagation 'mandatory'");
        } else if (((TransactionDefinition)definition).getPropagationBehavior() != 0 && ((TransactionDefinition)definition).getPropagationBehavior() != 3 && ((TransactionDefinition)definition).getPropagationBehavior() != 6) {
            if (((TransactionDefinition)definition).getIsolationLevel() != -1 && this.logger.isWarnEnabled()) {
                this.logger.warn("Custom isolation level specified but no actual transaction initiated; isolation level will effectively be ignored: " + definition);
            }

            boolean newSynchronization = this.getTransactionSynchronization() == 0;
            return this.prepareTransactionStatus((TransactionDefinition)definition, (Object)null, true, newSynchronization, debugEnabled, (Object)null);
        } else {
            AbstractPlatformTransactionManager.SuspendedResourcesHolder suspendedResources = this.suspend((Object)null);
            if (debugEnabled) {
                this.logger.debug("Creating new transaction with name [" + ((TransactionDefinition)definition).getName() + "]: " + definition);
            }

            try {
                boolean newSynchronization = this.getTransactionSynchronization() != 2;
                DefaultTransactionStatus status = this.newTransactionStatus((TransactionDefinition)definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
                this.doBegin(transaction, (TransactionDefinition)definition);
                this.prepareSynchronization(status, (TransactionDefinition)definition);
                return status;
            } catch (Error | RuntimeException var7) {
                this.resume((Object)null, suspendedResources);
                throw var7;
            }
        }
    }

当第一次执行crud时会走最后一个分支,通过doBegin方法的beginTransaction切换数据源,开启新的事务

Object transactionData = this.getJpaDialect().beginTransaction(em, new JpaTransactionManager.JpaTransactionDefinition(definition, timeoutToUse, txObject.isNewEntityManagerHolder()));

而当后续crud操作时,会走第一个分支,将后续操作包含在上一个事务中

if (this.isExistingTransaction(transaction)) {
	return this.handleExistingTransaction((TransactionDefinition)definition, transaction, debugEnabled);
}

这是因为在判断前执行的JpaTransactionManager.doGetTransaction方法内设置了EntityManagerHolder

protected Object doGetTransaction() {
    JpaTransactionManager.JpaTransactionObject txObject = new JpaTransactionManager.JpaTransactionObject();
    txObject.setSavepointAllowed(this.isNestedTransactionAllowed());
    EntityManagerHolder emHolder = (EntityManagerHolder)TransactionSynchronizationManager.getResource(this.obtainEntityManagerFactory());
    if (emHolder != null) {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Found thread-bound EntityManager [" + emHolder.getEntityManager() + "] for JPA transaction");
        }

        txObject.setEntityManagerHolder(emHolder, false);
    }

    if (this.getDataSource() != null) {
        ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(this.getDataSource());
        txObject.setConnectionHolder(conHolder);
    }

    return txObject;
}

是否重新获取数据库链接是在LogicalConnectionManagedImpl类的acquireConnectionIfNeeded方法中

private Connection acquireConnectionIfNeeded() {
    if (this.physicalConnection == null) {
        this.observer.jdbcConnectionAcquisitionStart();

        try {
            this.physicalConnection = this.jdbcConnectionAccess.obtainConnection();
        } catch (SQLException var5) {
            throw this.sqlExceptionHelper.convert(var5, "Unable to acquire JDBC Connection");
        } finally {
            this.observer.jdbcConnectionAcquisitionEnd(this.physicalConnection);
        }
    }

    return this.physicalConnection;
}

如果physicalConnection 为null,则重新链接数据源

总结

事务的开启过程:
如果方法上加了事务注解,spring会拦截注解、预先连接数据库,切换数据源,开启事务。后续CRUD操作则不会再切换数据源。会将后续CRUD操作包含在同一个数据源的整个事务中。

失效原因

我的情况是租户库CRUD操作用的时jdbcTemplate,会员库用的jpa。租户库的CRUD方法上加个切换数据源注解,方法执行前加ThreadLocal标识,方法执行后清除ThreadLocal标识。失效的原因是方法执行之前,预先开启事务时,ThreadLocal标识还未设定,则事务所使用的数据源为会员库,后续操作切换租户库失败。

解决办法

既然@Transactional也为注解,则可以调整注解执行的优先级。使得自定义注解优于@Transactional。自定义注解implements Ordered,调整优先级为最大

@Aspect
@Component
@Slf4j
public class DynamicDataSourceAspect implements Ordered {
	/**
	 * 配置切入点
	 */ @Pointcut("@annotation(com.mammoth.design.config.annotation.DataSource)")
	public void dataSourcePointcut() {
	    // 该方法无方法体,主要为了让同类中其他方法使用此切入点
	}

	//在注解方法执行前先切换数据源
	@Before("dataSourcePointcut()")
	public void changeDataSource(JoinPoint point){
	    String formKey = ContextHolder.getContext().getFormKey();
	    FormOriginService formOriginService= SpringContextHolder.getBean(FormOriginService.class);
	    FormOriginDto formOriginDto = formOriginService.findByFormKey(formKey);
	    ContextHolder.getContext().setAppId(formOriginDto.getApp().getId());
	}

    //方法执行后删除键值对
    @After("dataSourcePointcut()")
    public void restoreDataSource(JoinPoint point){
        ContextHolder.clearContext();
    }

    @Override
    public int getOrder() {
        return Integer.MIN_VALUE;
    }
}
  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot是一个用于构建独立的、可执行的Spring应用程序的框架,简化了Spring应用程序的配置和部署。JPA(Java Persistence API)是一种用于管理Java对象和关系数据库之间映射的规范。Druid是阿里巴巴开的关系型数据库连接池。 在Spring Boot中配置多数据需要以下几步: 1. 引入相关依赖:需要引入Spring Boot、Spring Data JPA和Druid的相关依赖。 2. 配置数据:在application.properties或application.yml文件中配置多个数据的连接信息,并指定每个数据的名称和相关属性。 3. 配置数据连接池:使用@ConfigurationProperties注解创建多个数据的连接池对象,并指定数据的名称以及相关属性。 4. 配置实体管理器工厂:为每个数据配置对应的实体管理器工厂,用于处理JPA实体与数据库之间的映射关系。 5. 配置事务管理器:为每个数据配置对应的事务管理器,用于处理事务操作。 6. 配置数据路由:创建动态数据,根据传入的数据名称选择对应的数据进行操作。 7. 配置JPA的Repository:创建接口继承JpaRepository,用于定义数据访问方法。 通过以上步骤配置好多数据后,就可以在Spring Boot应用程序中使用多个数据进行数据库的操作。可以根据需要在Service或Controller中使用@PersistenceContext注解指定具体的数据,或者使用@Primary注解指定默认的数据。 总结:通过Spring Boot的自动配置和Druid的连接池,可以很方便地实现多数据的配置。使用JPA进行数据操作,能够有效地减少开发人员编写SQL语句的工作量,提高开发效率。通过合理的配置,可以根据需要选择不同的数据进行操作,实现灵活的数据访问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值