During the recent project development, some colleagues reported that some declarative (note) transactions did not take effect in the current development system. After more than an hour’s investigation, we finally located the problem, which is recorded here.

preface

Technical background

The current system is developed using SpringBoot+JPA+Hibernate as the basic framework for development, of course, the overall also includes some permissions, Security and other information irrelevant to the investigation of this problem is not detailed, the following is the version of the core library information.

  • SpringBoot: 2.1.5. RELEASE
  • Hibernate: 5.3.10. Final

Different from the conventional system, this system adopts the scheme of dual data sources due to the particularity of business. Since it is dual data sources, the transaction manager will also be configured with two.

Dual data source implementation scheme

Based on the JPA implementation double data source is not trouble, only need to build two configuration class definition DataSource, SessionFactory, PlatformTransactionManager objects, including SessionFactory configured Dao layer scanning paths separate, Here is sample code for the configuration:

@Configuration
public class SystemDSConfiguration {

    @Resource
    private Environment environment;

    /** * System data source */
    @Primary
    @Bean
    public DataSource systemDataSource(a) {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(environment.getProperty("spring.datasource.system.jdbc-url"));
        druidDataSource.setUsername(environment.getProperty("spring.datasource.system.username"));
        druidDataSource.setPassword(environment.getProperty("spring.datasource.system.password"));
        druidDataSource.setDriverClassName(environment.getProperty("spring.datasource.system.driver-class-name"));
        return druidDataSource;
    }

    @Primary
    @Bean
    public SessionFactory systemSessionFactory(DataSource dataSource) {
        LocalSessionFactoryBuilder sessionFactoryBuilder = new LocalSessionFactoryBuilder(dataSource);
        sessionFactoryBuilder.scanPackages("com.zjcds.tj.server.system");
        sessionFactoryBuilder.setProperty(AvailableSettings.SHOW_SQL, "true");
        return sessionFactoryBuilder.buildSessionFactory();
    }

    /** * Configure hibernate transaction manager *@returnReturn transaction manager */
    @Primary
    @Bean
    public PlatformTransactionManager systemTransactionManager(DataSource dataSource) {
        return newJpaTransactionManager(systemSessionFactory(dataSource)); }}@Configuration
public class BusinessDSConfiguration {

    @Resource
    private Environment environment;

    private DataSource businessDataSource(a) {
        // ...
    }

    @Bean
    public SessionFactory businessSessionFactory(a) {
        // ...
    }

    @Bean
    public PlatformTransactionManager businessTransactionManager(a) {
        // ...}}Copy the code

Use of declarative transactions

Familiar with SpringBoot students should all know, use the declarative transaction only need to write on the service implementation class or function @ Transactional annotation, so the system for transaction is also very easy to use and the example code is as follows:

package com.zjcds.tj.server.business.service.impl;

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class BusinessService implements IBusinessService {

  private final BusinessDTO.DTOConverter businessDTOConverter = new BusinessDTO.DTOConverter();

  private final IBussinessDao businessDao;

  public BusinessVO saveBusiness(BusinessDTO dto) {
    log.debug("Save business {}", dto);
    BussinessEntity entity = businessDTOConverter.doForward(dto);
    bussinessDao.save(entity);
    throw new Exception("DB rollback test.");
  }

  // more code ...
}
Copy the code

BussinessEntity business entities should not be persisted to the database, but this article shows that the code is executing exactly the opposite of what is expected, so the next step is to ask why the transaction is not working.

Analysis of the

Before analyzing the reasons for this Transactional problem we need to understand how @Transactional works. I tried to search for @Transactional as a keyword in Google. After reading some articles, I found the following categories

  • speak@TransactionalUsage class 1 (how to use, declarative, imperative)
  • Talk about transaction conceptual class (transaction properties and behavior, etc.)
  • Talk about the principle of Spring to implement transactions and core class

Combined with the content of the search, I am ready to take a look at the source code in accordance with their own ideas, after all, there is no secret before the source code. Next I’ll describe how I found what I wanted from the source code.

Integrating search results

I talked about the content of the search, but I didn’t say what it was, but here are some things that I think are important in the search:

  1. SpringBoot can start transactions in two ways
  • Autoloading depends onTransactionAutoConfiguration
  • Manual enablement depends on@EnableTransactionManagemen
  1. Spring transactions operate based on AOP,TransactionInterceptorIs the implementation class of its facets
  2. The @Transactional provides configurations such asTransactionManager, rollbackForAnd so on,AnnotationTransactionAttributeSourceResponsible for reading these configurations
  3. PlatformTransactionManager as the definition of the transaction manager in the Spring, includingGetTransaction, COMMIT, rollbackThree methods

Now that you have this knowledge, we’ll move on to the Transactional source parsing section. Want to see the source code first, we need to find the entrance, we see the first knowledge first, because I do not have to use the project @ EnableTransactionManagemen annotations, so the transaction is completed automatically by the TransactionAutoConfiguration loading configuration.

SpringBoot auto-assembly can be implemented in two ways

  • The first is aSuch mechanism of SPI“By scanning the classes defined in the meta-INF/Spring.Factories file
  • The second is@ Import mechanismAssembly by scanning the classes defined in the @import annotation

The search for a TransactionAutoConfiguration

TransactionAutoConfiguration code is as follows, the code quantity is not much and I most comments are given, then how should we go to see the code?

In conjunction with the second point, Spring transactions are AOP based, and since they are AOP based there must be aspects defined.

SpringAOP can be implemented in two ways

  • The first is the JDK proxy
  • The second type is the Cglib proxy
@Configuration
// This configuration is loaded only when a type of Bean exists
@ConditionalOnClass(PlatformTransactionManager.class)
// indicates that classes in the configuration will be loaded after the current class is loaded
@AutoConfigureAfter({JtaAutoConfiguration.class, HibernateJpaAutoConfiguration.class,
        DataSourceTransactionManagerAutoConfiguration.class,
        Neo4jDataAutoConfiguration.class})
// Enable the @configurationProperties annotation
@EnableConfigurationProperties(TransactionProperties.class)
public class TransactionAutoConfiguration {

    // Define a Bean that is hosted in the Spring container. The function returns a value representing BeanType. The function name represents BeanName
    @Bean
    This configuration is loaded when there is no matching Bean in the Spring container
    @ConditionalOnMissingBean
    public TransactionManagerCustomizers platformTransactionManagerCustomizers( ObjectProvider
       
        > customizers)
       > {
        return new TransactionManagerCustomizers(
                customizers.orderedStream().collect(Collectors.toList()));
    }

    @Configuration
    If there are more than one, you need to mark the body with @primary
    @ConditionalOnSingleCandidate(PlatformTransactionManager.class)
    public static class TransactionTemplateConfiguration {

        private final PlatformTransactionManager transactionManager;

        public TransactionTemplateConfiguration( PlatformTransactionManager transactionManager) {
            this.transactionManager = transactionManager;
        }

        @Bean
        @ConditionalOnMissingBean
        public TransactionTemplate transactionTemplate(a) {
            return new TransactionTemplate(this.transactionManager); }}@Configuration
    // This configuration is loaded only if the Bean exists
    @ConditionalOnBean(PlatformTransactionManager.class)
    @ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
    // It's not hard to see the definition of AOP here. We'll look at this configuration file first
    public static class EnableTransactionManagementConfiguration {

        @Configuration
        // The transaction module is enabled manually here via annotations
        @EnableTransactionManagement(proxyTargetClass = false)
        // It takes effect when the specified value in the configuration file matches
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class",
                havingValue = "false", matchIfMissing = false)
        public static class JdkDynamicAutoProxyConfiguration {}@Configuration
        @EnableTransactionManagement(proxyTargetClass = true)
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class",
                havingValue = "true", matchIfMissing = true)
        public static class CglibAutoProxyConfiguration {}}}Copy the code

Observe the class you will soon be able to find a key, you must find aspect configuration on the bottom of EnableTransactionManagementConfiguration this class, and the class using the @ EnableTransactionManagement this annotation, It’s also the annotation we mentioned earlier about manually opening transactions, so we’ll have to keep an eye on it anyway.

@ EnableTransactionManagement inquiry

First we see @ EnableTransactionManagement code

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {

    // Whether to create a cglib-based proxy, otherwise create JDK code
    boolean proxyTargetClass(a) default false;

    // How should transaction notification Proxy or Aspectj be used
    AdviceMode mode(a) default AdviceMode.PROXY;

    int order(a) default Ordered.LOWEST_PRECEDENCE;
}
Copy the code

For me, the code is a valid information, loaded TransactionManagementConfigurationSelector the configuration class, so take a look at its code

AdviceModeImportSelector Provides a policy for configuring the switch based on the value of AdviceMode
public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> {


    @Override
    protected String[] selectImports(AdviceMode adviceMode) {
        switch (adviceMode) {
            case PROXY:
                // Implement section configuration based on JDK proxy by default
                return new String[]{AutoProxyRegistrar.class.getName(),
                        ProxyTransactionManagementConfiguration.class.getName()};
            case ASPECTJ:
                // Implement section configuration based on ASPECTJ
                return new String[]{determineTransactionAspectClass()};
            default:
                return null; }}private String determineTransactionAspectClass(a) {
        return (ClassUtils.isPresent("javax.transaction.Transactional", getClass().getClassLoader()) ? TransactionManagementConfigUtils.JTA_TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME : TransactionManagementConfigUtils.TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME); }}Copy the code

If no special configuration, here will be the default by the JDK dynamic proxy, therefore, AutoProxyRegistrar ProxyTransactionManagementConfiguration these two configuration classes will be loaded

  1. AutoProxyRegistrar
@Override
public void registerBeanDefinitions(..) {...if (mode == AdviceMode.PROXY) {
        AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
        if ((Boolean) proxyTargetClass) {
            AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
            return; }}... }Copy the code
  1. ProxyTransactionManagementConfiguration
@Configuration
public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration {

    @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)
    // Used to classify beans
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    // transaction enhancer
    public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor(a) {
        BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
        advisor.setTransactionAttributeSource(transactionAttributeSource());
        advisor.setAdvice(transactionInterceptor());
        if (this.enableTx ! =null) {
            advisor.setOrder(this.enableTx.<Integer>getNumber("order"));
        }
        return advisor;
    }

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    // Transaction annotation information read
    public TransactionAttributeSource transactionAttributeSource(a) {
        return new AnnotationTransactionAttributeSource();
    }

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    // method interceptor
    public TransactionInterceptor transactionInterceptor(a) {
        TransactionInterceptor interceptor = new TransactionInterceptor();
        interceptor.setTransactionAttributeSource(transactionAttributeSource());
        if (this.txManager ! =null) {
            interceptor.setTransactionManager(this.txManager);
        }
        returninterceptor; }}Copy the code

So far we have found the transaction method interceptorTransactionInterceptorWhat does our method do when it is intercepted by a transaction?

TransactionInterceptor inquiry

Reading the TransactionInterceptor class, it’s easy to see that invoke is the main implementation of transaction interception. It’s just two lines of code that invoke the invokeWithinTransaction method of the parent class.

The MethodInterceptor method intercepts the invoke interface as a concrete implementation function
public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor.Serializable {

    public TransactionInterceptor(a) {}public TransactionInterceptor(PlatformTransactionManager ptm, Properties attributes) {
        setTransactionManager(ptm);
        setTransactionAttributes(attributes);
    }

    public TransactionInterceptor(PlatformTransactionManager ptm, TransactionAttributeSource tas) {
        setTransactionManager(ptm);
        setTransactionAttributeSource(tas);
    }


    @Override
    @Nullable
    // The concrete implementation of the transaction method interception invokeWithinTransaction method of the parent class
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // Get the target class of the proxyClass<? > targetClass = (invocation.getThis() ! =null ? AopUtils.getTargetClass(invocation.getThis()) : null);
        return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
    }


    //---------------------------------------------------------------------
    // Serialization support
    //---------------------------------------------------------------------
    private void writeObject(ObjectOutputStream oos) throws IOException {
        // ...
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        // ...}}Copy the code

Then we look at the invokeWithinTransaction method of the parent class. Although there is a lot of code, combined with the configuration of the project, we will soon find that because we are using JpaTransactionManager, the following conditions are not included. So we only need to look at the front of a small piece of code, this code has a function to obtain the transaction manager called determineTransactionManager, under the dual data source of transaction failure is likely to be on the transaction manager access error.

// BeanFactoryAware provides the BeanFactory resource for the current class
// InitializingBean Will be notified when the current class construction is complete
public abstract class TransactionAspectSupport implements BeanFactoryAware.InitializingBean {

    @Nullable
    protected Object invokeWithinTransaction(Method method, @Nullable Class<? > targetClass,final InvocationCallback invocation) throws Throwable {

        TransactionAttributeSource tas = getTransactionAttributeSource();
        // Read the attributes defined in @Transactional
        finalTransactionAttribute txAttr = (tas ! =null ? tas.getTransactionAttribute(method, targetClass) : null);
        // Get a transaction manager based on the defined properties
        final PlatformTransactionManager tm = determineTransactionManager(txAttr);
        final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
        if (txAttr == null| |! (tminstanceof CallbackPreferringPlatformTransactionManager)) {
            // Since this project is configured with JpaTransactionManager it will enter this code block
            // Create transactions if necessary
            TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);

            Object retVal;
            try {
                // Execute the proxied function and get the result
                retVal = invocation.proceedWithInvocation();
            }
            catch (Throwable ex) {
                // Roll back the transaction and throw the exception as is
                completeTransactionAfterThrowing(txInfo, ex);
                throw ex;
            }
            finally {
                cleanupTransactionInfo(txInfo);
            }
            // Commit the transaction and return the result
            commitTransactionAfterReturning(txInfo);
            return retVal;
        }

        else {
            final ThrowableHolder throwableHolder = new ThrowableHolder();
            // ... more code}}}Copy the code

DetermineTransactionManager we see some of this method is based on @ Transactional attributes to obtain the transaction manager, and then see how it is done.

@Nullable
protected PlatformTransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) {
    if (txAttr == null || this.beanFactory == null) {
        // Returns a predefined transaction manager in the method interceptor
        return getTransactionManager();
    }

    String qualifier = txAttr.getQualifier();
    if (StringUtils.hasText(qualifier)) {
        // Read the qualifier property. If it is not empty, go to the Bean factory and get the transaction manager with the corresponding name
        return determineQualifiedTransactionManager(this.beanFactory, qualifier);
    } else if (StringUtils.hasText(this.transactionManagerBeanName)) {
        // If a name is defined in the method interceptor, the transaction manager with the same name in the container is used
        return determineQualifiedTransactionManager(this.beanFactory, this.transactionManagerBeanName);
    } else {
        PlatformTransactionManager defaultTransactionManager = getTransactionManager();
        if (defaultTransactionManager == null) {
            defaultTransactionManager = this.transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY);
            if (defaultTransactionManager == null) {
                // If neither match, get the transaction manager defined as the principal
                defaultTransactionManager = this.beanFactory.getBean(PlatformTransactionManager.class);
                this.transactionManagerCache.putIfAbsent( DEFAULT_TRANSACTION_MANAGER_KEY, defaultTransactionManager); }}returndefaultTransactionManager; }}Copy the code

Problems surface

After debug analysis, I found the cause of transaction failure. Because my Bussiness module acquired the transaction manager of System module, its transaction failed. Some of you may be wondering here, will getting the wrong transaction manager not report an error?

So we can look at this one this way, think about how we start transactions in mysql.

mysql> begin; Database A starts the transaction
Query OK, 0 rows affected (0.00 sec)
 
mysql> delete from sys_user where id = 1; Database A executes the delete scriptQuery OK, 1 row affected (0.01sec) mysql> rollback;Database A rollbackQuery OK, 0 rows affected (0.01sec) mysql>Copy the code

Spring also executes scripts remotely when using transactions, except that the process looks like this: database A starts A transaction and we execute scripts on database B. Theoretically, no errors are reported, but the transaction on database B definitely does not take effect.

mysql> begin; Database A starts the transaction
Query OK, 0 rows affected (0.00 sec)
 
mysql> delete from sys_user where id = 1; Database B executes the delete scriptQuery OK, 1 row affected (0.01sec) mysql> rollback;Database A rollbackQuery OK, 0 rows affected (0.01sec) mysql>Copy the code

The solution

Now that the problem has been identified, it’s time to consider how to solve it. As mentioned earlier, @transactional has a configuration called transactionManager that specifies which transactionManager to use for the current transaction-enabled method.

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

	@AliasFor("transactionManager")
	String value(a) default "";

	@AliasFor("value")
	String transactionManager(a) default "";

	// ...
}
Copy the code

The simplest solution is to annotate all Service implementation classes with @Transactional annotations that specify the Transactional manager name. However, we are nearing the end of system development. I don’t want to change files one by one.

There is, of course, a way to do this. Rearrange the previously mentioned process for acquiring a transaction manager, which looks something like this:

  1. TransactionAttributeSourceComponent is responsible for parsing@TransactionalThe configuration ofTransactionAttribute
  2. readTransactionAttributeIn thequalifierUsed to get a named transaction manager

If we can replace a TransactionAttributeSource implementation make it according to the package name return different transaction manager name, can complete the above requirements. To replace we have to know where it is defined, actually in the process of reading the source code we have already seen, ProxyTransactionManagementConfiguration this class is responsible for the definition, We only need to define a can replace the ProxyTransactionManagementConfiguration, Because of TransactionAutoConfiguration @ ConditionalOnMissingBean (AbstractTransactionManagementConfiguration. Class) limited, And AbstractTransactionManagementConfiguration namely is the parent class we need custom configuration, the custom after transaction automation configuration will not load EnableTransactionManagementConfiguration, So there is no problem with Bean definition conflicts.

public class TransactionAutoConfiguration {

    @Configuration
    @ConditionalOnBean(PlatformTransactionManager.class)
    @ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
    public static class EnableTransactionManagementConfiguration {
      // more code...
    }

    // more code
}
Copy the code

Without further ado directly to the code, this configuration can be implemented by the package name prefix dynamic switch transaction manager, sprinkle flower end.

@Configuration
public class MyProxyTransactionManagementConfiguration extends ProxyTransactionManagementConfiguration {


    /** * Rewrites the transaction annotation attribute parser * to dynamically get the transaction manager name based on the package name * to ensure that multi-source transactions can run properly ** without modifying the Service code@returnTransaction annotation property parser */
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    @Override
    public TransactionAttributeSource transactionAttributeSource(a) {
        return new AnnotationTransactionAttributeSource() {
            @Override
            public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class
        targetClass) {
                DefaultTransactionAttribute attribute = (DefaultTransactionAttribute) super.getTransactionAttribute(method, targetClass);
                if(attribute ! =null && attribute.getQualifier() == null) {
                    // Get the full name of the class to which the method belongs
                    String name = method.getDeclaringClass().getName();
                    if (name.startsWith("com.zjcds.tj.server.system")) {
                        attribute.setQualifier("systemTransactionManager");
                    } else if (name.startsWith("com.zjcds.tj.server.business")) {
                        attribute.setQualifier("businessTransactionManager"); }}returnattribute; }}; }}Copy the code

About the Official Account