Spring Transaction Management

After understanding the characteristics of transactions, we will find that transactions are actually a lot of knowledge, but the puzzle is that we usually use Springboot in the development project, we almost rarely write transaction related things, this is because Spring helps us to do transaction management. However, Spring’s transaction management can help us to do the most basic transaction management, if our business becomes more and more complex, multi-data source scenarios or the need for refined transaction management, we need to handle it ourselves.

In fact, Spring uses transaction propagation behavior to manage transactions while processing transactions. For an introduction to the transaction itself, refer to transaction isolation level and transaction concurrency issues. Let’s focus on spring’s transaction propagation behavior. Transaction propagation behavior is a spring concept for managing transactions, not database transactions, to be clear. Example of an explanation of transaction propagation behavior.

Spring transaction propagation behavior

Propagation behavior describe
PROPAGATION_REQUIRED If a transaction exists, join the transaction. If there is no transaction currently, a new transaction is created.The default value
PROPAGATION_REQUIRES_NEW Creates a new transaction and suspends the current transaction if one exists
PROPAGATION_SUPPORTS If a transaction exists, join the transaction. If there is no transaction currently, it continues in a non-transactional manner
PROPAGATION_NOT_SUPPORTED Runs nontransactionally and suspends the current transaction if one exists
PROPAGATION_NEVER Runs nontransactionally and throws an exception if a transaction currently exists
PROPAGATION_MANDATORY If a transaction exists, join the transaction. If there is no transaction currently, an exception is thrown
PROPAGATION_NESTED If a transaction exists, a transaction is created to run as a nested transaction of the current transaction; If no current affairs, the value of equivalent to the TransactionDefinition. PROPAGATION_REQUIRED

The spring transaction

In the early use of SpringMVC, AOP is also used for transaction processing, but are based on XML way, this way requires too much configuration and workload, and after the application of SpringBoot, can better use annotations to deal with, so XML way is not used.XML configuration code


      
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="Http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">

    <! Load the resource file, which contains variable information, must be loaded first in the Spring configuration file.
    <context:property-placeholder location="classpath:mysql.properties" />
    
    <bean id="sessionFactory" 
        class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="packagesToScan">
            <list>
                <value>com.troyqu.entity</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>
                <prop key="hibernate.dialect">${hibernate.dialect}</prop>
                <prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
            </props>
        </property>
    </bean>
    
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
      <property name="driverClassName" value="${jdbc.driverClassName}" />
      <property name="url" value="${jdbc.url}" />
      <property name="username" value="${jdbc.user}" />
      <property name="password" value="${jdbc.pass}" />
   </bean>
   
    <! Configure Hibernate transaction manager -->
    <bean id="transactionManager"
        class="org.springframework.orm.hibernate4.HibernateTransactionManager">
      <property name="sessionFactory" ref="sessionFactory" />
   </bean>
   
   <! TransactionManager -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="add*" propagation="REQUIRED" />
            <tx:method name="get*" propagation="REQUIRED" />
            <tx:method name="*" read-only="true" />
        </tx:attributes>
    </tx:advice>
    
    <aop:config expose-proxy="true">
        <! -- Config pointcut -->
        <aop:pointcut id="txPointcut" expression="execution(* com.troyqu.service.. *. * (..) )" />
        <! -- Configure pointcuts and advice -->
        <aop:advisor pointcut-ref="txPointcut" advice-ref="txAdvice"/>
    </aop:config>
    
</beans>
Copy the code

Aop-based Spring transactions

It is also possible to implement transaction management in SpringBoot using custom annotations in conjunction with AOP, which is simpler and saves code than the XML approach. The AOP configuration here does not mean declarative annotations using @Transactional. This refers to the AOP approach combined with @aspect. We can take a look at the key code.

The database connection class MyConnection is used to manage database connections. Using ThreadLocal to manage database connections eliminates the need to create multiple database connections in the same thread.

package com.troyqu.aop;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.NamedThreadLocal;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.sql.SQLException;

@Component
public class MyConnection {

    @Autowired
    DataSourceTransactionManager dataSourceTransactionManager;

    ThreadLocal<Connection> connection = new NamedThreadLocal<>("MyConnection");

    public Connection getConnection(a){
        Connection conn = connection.get();
        if(conn ! =null) {
            return conn;
        }
        try {
             conn = dataSourceTransactionManager.getDataSource().getConnection();
             connection.set(conn);
            return conn;
        } catch (SQLException e) {
            e.printStackTrace();
            return null; }}public void closeConnection(a){
        Connection conn = connection.get();
        if(conn ! =null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }finally{ connection.remove(); } } connection.remove(); }}Copy the code

Custom annotations serve as transaction declaration pointcuts

public @interface MyTransactional {
}
Copy the code

Create the transaction processing AOP class TransactionAop to handle methods marked @MyTransactional

package com.troyqu.aop;

import lombok.val;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.sql.SQLException;

@Aspect
@Component
public class TransactionAop {

    @Autowired
    MyConnection myConnection;

    @Around("@annotation(com.anchnet.smartops.cmp.aop.MyTransactional)")
    public Object txAround(ProceedingJoinPoint pjp) throws Throwable {
        Connection conn = myConnection.getConnection();
        Object output = null;
        try {
            beginTransaction(conn);
            output = pjp.proceed();
            commitTransaction(conn);
        } catch (Throwable e) {
            rollBackTransaction(conn);
            System.out.println("Error happened inside TX");
            e.printStackTrace();
        }finally {
            myConnection.closeConnection();
            returnoutput; }}private void beginTransaction(Connection conn){
        try {
            conn.setAutoCommit(false);
        } catch (SQLException e) {
            System.out.println("Start TX failed"); e.printStackTrace(); }}private void commitTransaction(Connection conn){
        try {
            conn.commit();
        } catch (SQLException e) {
            System.out.println("Commit failed"); e.printStackTrace(); myConnection.closeConnection(); }}private void rollBackTransaction(Connection conn){
        try {
            conn.rollback();
        } catch (SQLException e) {
            System.out.println("Rollback failed"); e.printStackTrace(); myConnection.closeConnection(); }}}Copy the code

Declarative transaction

Now you can use more simple declarative transaction to transaction processing, as long as open the @ EnableTransactionManagement can use declarative transaction, spring – the boot – autoconfigure will introduce relevant Bean we can only be used, To enable Transactional support, simply add @Transactional to the corresponding method and class name. Propagation @transactional allows you to set many attributes, including transactionManager, transaction propagation, etc. The transaction manager can choose the default transaction manager when using a single data element, but if we use multiple data sources, we need to manage the corresponding transaction manager.

An exception occurred to rollback the transaction

Transactional using @Transactional, you can specify a rollback strategy, such as the following code to roll back when an Exception is thrown.

Multiple transaction manager Specifies the corresponding transaction manager

Specify different transaction managers for method1 and method2, respectively.

    @Transactional(value="txManager1")
    public void method1(Connection conn){
        System.out.println("this is method1");
    }
    
    @Transactional(value="txManager2")
    public void method2(Connection conn){
        System.out.println("this is method2");
    }    
Copy the code

Here are a few examples to look at transaction processing in a specific scenario. For the sake of simplicity, we just output output to simulate real database operations. Database operations in method1 and method2 are rolled back because method1 and method2 are in a transaction with the same @Transactional declaration.

    @Transactional
    public void tx(a){
        method1();
        method2();
    }

    public void method1(a){
        System.out.println("this is method1");
        int i = 10 / 0;
    } 

    public void method2(a){
        System.out.println("this is method2");
    }    
Copy the code

Method1 and method2 use the @Transactional annotation in the same transaction as method1 and Method2. This is not possible because spring transaction default transmission mechanism is: TransactionDefinition. PROPAGATION_REQUIRED if a transaction exists, to join the transaction; If there is no transaction currently, a new transaction is created.

    @Transactional
    public void tx(a){
        method1();
        method2();
    }

    @Transactional
    public void method1(a){
        System.out.println("this is method1");
        int i = 10 / 0;
    } 

    @Transactional
    public void method2(a){
        System.out.println("this is method2");
    }    
Copy the code

Isn’t there any way to isolate transactions after using @transacal? Method1 and method2 require a Propagation mechanism for Propagation.REQUIRES_NEW. Even though the outermost methods have @transactional annotations, But Propagation.REQUIRES_NEW creates a new transaction for method1 and method2, in which case method1 rolls back and method2 handles normally.

    @Transactional
    public void tx(a){
        method1();
        method2();
    }

    @Transactional(propagation= Propagation.REQUIRES_NEW)
    public void method1(a){
        System.out.println("this is method1");
        int i = 10 / 0;
    } 

    @Transactional(propagation= Propagation.REQUIRES_NEW)
    public void method2(a){
        System.out.println("this is method2");
    }    
Copy the code

So far, we have had a general understanding of transaction control. The specific control of transaction must be flexibly used in combination with our business. If we encounter multiple data sources, we also need to take the transaction manager into account.

Transactional Transactional failure scenario

  • There is an exception that needs to be thrown. If a catch exception is not followed by a throw then the transaction is not rolled back because the exception has already been caught so for @Transactional methods no exception is thrown;
@Transactional(rollbackFor = Exception.class)
@Override
public void unbind(Integer crmId) {
    try{ checkIf(! UserUtils.isOpsUser(), AmsCode.USER_NOT_OPS); val crmCustomer = customerMapper.selectByPrimaryKey(crmId); checkIf(crmCustomer ==null, AmsCode.CRM_ACCOUNT_NOT_FOUND); checkIf(crmCustomer.getStatus() ! =null && !CrmCustomerStatus.bound.name().equalsIgnoreCase(crmCustomer.getStatus()), AmsCode.CRM_CUSTOMER_NOT_BIND_ACCOUNT);
        val account = accountMapper.selectOne(new AmsAccount().setCrmId(crmId));
        if(account ! =null) {
            account.setCrmId(null);
            accountMapper.updateByPrimaryKey(account);
        }
        customerMapper.updateByPrimaryKey(crmCustomer.setStatus(CrmCustomerStatus.unbound.name()));
    } catch (Exception e) {
       // No exception rollback occurs because the exception is caught and not thrown again
       log.error("error happened {}", e); }}Copy the code

We can throw an exception in a catch if we want to log an error as well as handle a transaction.

  • Methods must be public modifiers. If they are not public modifiers, no error is reported, but the annotations do not take effect (because the dynamic proxy JDK’s dynamic proxy handling interface does not contain non-public methods, and cglib handles public methods).
  • Method annotations called through this do not take effect. Calls made inside the current class through this do not create a new proxy, so the annotations do not take effect.

conclusion

  • Springboot provides dependencies on transaction management by means of auto-assembly. However, if our business scenarios are complex and encounter multi-data source scenarios, it needs to be flexibly used in combination with actual services.
  • To implement Transactional transactions, you can configure transaction manager (Value) and transaction propagation properties to control the business scenarios that meet your needs.