Let's talk about 12 scenarios of spring transaction failure

Posted by kindoman on Thu, 04 Nov 2021 02:50:17 +0100

preface

For students engaged in java development, spring affairs must be familiar.

In some business scenarios, the data of multiple tables needs to be written simultaneously in one request. In order to ensure the atomicity of operations (success or failure at the same time) and avoid data inconsistency, we generally use spring transactions.

Indeed, spring transactions are fun to use. You can easily handle transactions with a simple annotation: @ Transactional. I guess most of my friends use it like this, and it's always cool.

But if you don't use it properly, it will pit you.

Today we'll talk about some scenarios of transaction failure. Maybe you've been caught. No, let's have a look.

1. The transaction does not take effect

1. Access rights

As we all know, there are four main access permissions for java: private, default, protected and public. Their permissions increase from left to right.

However, if we define some transaction methods with wrong access permissions in the development process, the transaction function will be faulty, for example:

@Service
public class UserService {
    
    @Transactional
    private void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }
}

We can see that the access permission of the add method is defined as private, which will lead to transaction failure. spring requires that the proxy method must be public.

To put it bluntly, there is a judgment in the computeTransactionAttribute method of AbstractFallbackTransactionAttributeSource class. If the target method is not public, the TransactionAttribute returns null, that is, transactions are not supported.

protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
    // Don't allow no-public methods as required.
    if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
      return null;
    }

    // The method may be on an interface, but we need attributes from the target class.
    // If the target class is null, the method will be unchanged.
    Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

    // First try is the method in the target class.
    TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
    if (txAttr != null) {
      return txAttr;
    }

    // Second try is the transaction attribute on the target class.
    txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
    if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
      return txAttr;
    }

    if (specificMethod != method) {
      // Fallback is to look at the original method.
      txAttr = findTransactionAttribute(method);
      if (txAttr != null) {
        return txAttr;
      }
      // Last fallback is the class of the original method.
      txAttr = findTransactionAttribute(method.getDeclaringClass());
      if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
        return txAttr;
      }
    }
    return null;
  }

In other words, if the access permission of our custom transaction method (i.e. target method) is not public but private, default or protected, spring will not provide transaction function.

2. The method was modified with final

Sometimes, when a method does not want to be subclassed, it can be defined as final. It is OK to define a normal method in this way, but if you define a transaction method as final, for example:

@Service
public class UserService {

    @Transactional
    public final void add(UserModel userModel){
        saveData(userModel);
        updateData(userModel);
    }
}

We can see that the add method is defined as final, which will lead to transaction invalidation.

Why?

If you have seen the source code of spring transactions, you may know that aop is used at the bottom of spring transactions, that is, through jdk dynamic proxy or cglib, it helps us generate proxy classes and realize the transaction functions in proxy classes.

However, if a method is decorated with final, the method cannot be overridden in its proxy class and transaction functions can be added.

Note: if a method is static, it cannot become a transaction method through dynamic proxy.

3. Method internal call

Sometimes we need to call another transaction method in a method in a Service class, for example:

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

  
    public void add(UserModel userModel) {
        userMapper.insertUser(userModel);
        updateStatus(userModel);
    }

    @Transactional
    public void updateStatus(UserModel userModel) {
        doSameThing();
    }
}

We can see that in the transaction method add, the transaction method updateStatus is called directly. As can be seen from the previous introduction, the updateStatus method has the ability to generate transactions because spring aop generates proxy objects, but this method directly calls the method of this object, so the updateStatus method will not generate transactions.

It can be seen that direct internal call of methods in the same class will lead to transaction failure.

So the question is, what if some scenarios really want to call another method in one way of the same class?

3.1 add a new Service method

This method is very simple. You only need to add a new Service method, add the @ Transactional annotation to the new Service method, and move the code that needs transaction execution to the new method. The specific codes are as follows:

@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceB serviceB;

   public void save(User user) {
         queryData1();
         queryData2();
         serviceB.doSave(user);
   }
 }

 @Servcie
 public class ServiceB {

    @Transactional(rollbackFor=Exception.class)
    public void doSave(User user) {
       addData1();
       updateData2();
    }

 }

3.2 inject yourself into the Service class

If you don't want to add a new Service class, injecting yourself into the Service class is also an option. The specific codes are as follows:

@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceA serviceA;

   public void save(User user) {
         queryData1();
         queryData2();
         serviceA.doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       addData1();
       updateData2();
    }
 }

Some people may have this question: will this approach lead to circular dependency?

Answer: No.

In fact, the three-level cache inside spring ioc ensures that it will not have circular dependency. But there are some holes. If you want to learn more about circular dependency, you can see my previous article< spring: how do I solve circular dependencies?>.

3.3 passing AopContent class

Use AopContext.currentProxy() in this Service class to get the proxy object

The above method 2 can solve the problem, but the code is not intuitive. You can also use AOPProxy to obtain the proxy object in the Service class to achieve the same function. The specific code is as follows:

@Servcie
public class ServiceA {

   public void save(User user) {
         queryData1();
         queryData2();
         ((ServiceA)AopContext.currentProxy()).doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       addData1();
       updateData2();
    }
 }

4. Not managed by spring

In our normal development process, one detail is easy to be ignored, that is, the premise of using spring transactions is that the object needs to be managed by spring and the bean instance needs to be created.

Generally, we can automatically implement bean instantiation and dependency injection through @ Controller, @ Service, @ Component, @ Repository and other annotations.

Of course, there are many methods to create bean instances. Interested partners can take a look at another article I wrote earlier< @Do you know all these operations of Autowired?>

If one day you rush to develop a Service class, but forget to add @ Service annotation, such as:

//@Service
public class UserService {

    @Transactional
    public void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }    
}

From the above example, we can see that if the UserService class is not annotated with @ Service, the class will not be managed by spring, so its add method will not generate transactions.

5. Multi thread call

In the actual project development, there are many multithreading scenarios. If spring transactions are used in multithreading scenarios, will there be any problems?

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        new Thread(() -> {
            roleService.doOtherThing();
        }).start();
    }
}

@Service
public class RoleService {

    @Transactional
    public void doOtherThing() {
        System.out.println("preservation role Table data");
    }
}

From the above example, we can see that the transaction method add calls the transaction method doOtherThing, but the transaction method doOtherThing is invoked in another thread.

In this way, the two methods are not in the same thread, and the database connections obtained are different, but two different transactions. If you want to throw an exception in the doOtherThing method, it is impossible for the add method to roll back.

If you have seen the source code of spring transactions, you may know that spring transactions are implemented through database connection. A map is saved in the current thread, key is the data source and value is the database connection.

private static final ThreadLocal<Map<Object, Object>> resources =

  new NamedThreadLocal<>("Transactional resources");

When we talk about the same transaction, we actually mean the same database connection. Only if we have the same database connection can we commit and rollback at the same time. If we get different database connections in different threads, they are different transactions.

6. Table does not support transactions

As we all know, before mysql5, the default database engine was myisam.

Its advantages needless to say: index files and data files are stored separately. For single table operations with more queries and less writes, the performance is better than innodb.

Some old projects may still use it.

When creating a table, you only need to set the ENGINE parameter to MyISAM:

CREATE TABLE `category` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin

myisam is easy to use, but there is a fatal problem: it does not support transactions.

If only the single table operation is good, there will be no big problems. However, if you need to operate across multiple tables, the data is likely to be incomplete because it does not support transactions.

In addition, myisam does not support row locks and foreign keys.

Therefore, in the actual business scenario, myisam does not use much. After mysql5, myisam has gradually withdrawn from the stage of history and replaced innodb.

Sometimes in the process of development, we find that the transaction of a table has not been effective, which is not necessarily the pot of spring transaction. It's best to confirm whether the table you use supports transactions.

7. Transaction not opened

Sometimes, the root cause of a transaction not taking effect is that the transaction is not opened.

You may find this sentence funny.

Isn't starting a transaction the most basic function in a project?

Why is the transaction not started?

Yes, if the project has been built, there must be transaction functions.

However, if you are building a project demo, there is only one table, and the transactions of this table do not take effect. So what causes it?

Of course, there are many reasons, but the reason why the transaction is not started is extremely easy to be ignored.

If you are using the springboot project, you are lucky. Because springboot has silently started the transaction for you through the DataSourceTransactionManagerAutoConfiguration class.

All you need to do is to configure the spring.datasource parameters.

However, if you are still using a traditional spring project, you need to manually configure transaction related parameters in the applicationContext.xml file. If you forget the configuration, the transaction will not take effect.

The specific configuration information is as follows:

   
<!-- Configure transaction manager --> 
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> 
    <property name="dataSource" ref="dataSource"></property> 
</bean> 
<tx:advice id="advice" transaction-manager="transactionManager"> 
    <tx:attributes> 
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes> 
</tx:advice> 
<!-- Cut in transactions with pointcuts --> 
<aop:config> 
    <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/> 
    <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/> 
</aop:config> 

Silently, if the pointcut matching rule in the pointcut tag is mismatched, some transactions will not take effect.

Second, the transaction is not rolled back

1. Error propagation characteristics

In fact, when using @ Transactional annotation, we can specify the propagation parameter.

This parameter is used to specify the propagation characteristics of transactions. spring currently supports seven propagation characteristics:

  • REQUIRED   If there is a transaction in the current context, join the transaction. If there is no transaction, create a transaction, which is the default propagation attribute value.

  • SUPPORTS   If there is a transaction in the current context, it is supported to join the transaction. If there is no transaction, it is executed in a non transaction manner.

  • MANDATORY   If there is a transaction in the current context, otherwise an exception is thrown.

  • REQUIRES_NEW   A new transaction will be created each time, and the transaction in the context will be suspended at the same time. After the current new transaction is completed, the context transaction will be resumed and executed again.

  • NOT_SUPPORTED   If a transaction exists in the current context, the current transaction is suspended and the new method is executed in an environment without a transaction.

  • NEVER   Throw an exception if there is a transaction in the current context, otherwise execute the code in a transaction free environment.

  • NESTED   If there is a transaction in the current context, the nested transaction is executed. If there is no transaction, a new transaction is created.

If we set the propagation property incorrectly when manually setting the propagation parameter, for example:

@Service
public class UserService {

    @Transactional(propagation = Propagation.NEVER)
    public void add(UserModel userModel) {
        saveData(userModel);
        updateData(userModel);
    }
}

We can see that the transaction propagation feature of the add method is defined as Propagation.NEVER. This type of propagation feature does not support transactions. If there are transactions, exceptions will be thrown.

At present, only these three propagation features can create new transactions: REQUIRED and REQUIRES_NEW,NESTED.

2. He swallowed it himself

The transaction will not be rolled back. The most common problem is that developers manually try...catch exceptions in the code. For example:

@Slf4j
@Service
public class UserService {
    
    @Transactional
    public void add(UserModel userModel) {
        try {
            saveData(userModel);
            updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

In this case, of course, the spring transaction will not be rolled back, because the developer caught the exception and did not throw it manually. In other words, he swallowed the exception.

If you want a spring transaction to roll back normally, you must throw an exception that it can handle. If no exceptions are thrown, spring thinks the program is normal.

3. Other exceptions are thrown manually

Even if the developer does not catch the exception manually, the spring transaction will not roll back if the exception thrown is incorrect.

@Slf4j
@Service
public class UserService {
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
        try {
             saveData(userModel);
             updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }
}

In the above case, the developer caught the Exception and threw the Exception manually: Exception. The transaction will not be rolled back.

Because of spring transactions, only RuntimeException (runtime Exception) and Error (Error) will be rolled back by default. For ordinary Exception (non runtime Exception), it will not be rolled back.

4. Customized rollback exception

When declaring a transaction with @ Transactional annotation, sometimes we want to customize the rollback exception, which spring also supports. You can complete this function by setting the rollbackFor parameter.

However, if the value of this parameter is set incorrectly, some inexplicable problems will arise, such as:

@Slf4j
@Service
public class UserService {
    
    @Transactional(rollbackFor = BusinessException.class)
    public void add(UserModel userModel) throws Exception {
       saveData(userModel);
       updateData(userModel);
    }
}

If the program reports an error and throws SqlException, DuplicateKeyException and other exceptions when executing the above code to save and update data. The BusinessException is our custom exception. The exception reporting an error does not belong to BusinessException, so the transaction will not be rolled back.

Even though there is a default value for rollback for, Alibaba developer specification still requires developers to specify this parameter again.

Why?

Because if the default value is used, once the program throws an Exception, the transaction will not be rolled back, which will cause a big bug. Therefore, it is recommended to set this parameter to Exception or Throwable in general.

5. Too many nested transaction rollbacks

public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        roleService.doOtherThing();
    }
}

@Service
public class RoleService {

    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing() {
        System.out.println("preservation role Table data");
    }
}

In this case, nested internal transactions are used. The original intention is to call the roleService.doOtherThing method. If an exception occurs, only the contents in the doOtherThing method will be rolled back, not the contents in userMapper.insertUser, that is, the savepoint will be rolled back.. But the truth is, insertUser also rolled back.

why?

Because there is an exception in the doOtherThing method, it is not captured manually. It will continue to throw up and catch the exception in the proxy method of the outer add method. Therefore, in this case, the entire transaction is rolled back directly, not just a single savepoint.

How can I rollback only savepoints?

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {

        userMapper.insertUser(userModel);
        try {
            roleService.doOtherThing();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

You can put internal nested transactions in try/catch without throwing exceptions up. This ensures that if an exception occurs in an internal nested transaction, only the internal transaction will be rolled back without affecting the external transaction.

III. others

1 major business issues

When using spring transactions, there is a very troublesome problem, that is, the big transaction problem.

Usually, we will annotate @ Transactional on the method and add transaction functions, such as:

@Service
public class UserService {
    
    @Autowired 
    private RoleService roleService;
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
       query1();
       query2();
       query3();
       roleService.save(userModel);
       update(userModel);
    }
}


@Service
public class RoleService {
    
    @Autowired 
    private RoleService roleService;
    
    @Transactional
    public void save(UserModel userModel) throws Exception {
       query4();
       query5();
       query6();
       saveData(userModel);
    }
}

However, if the @ Transactional annotation is added to the method, the disadvantage is that the whole method is included in the transaction.

In the above example, in the UserService class, only these two lines need transactions:

roleService.save(userModel);
update(userModel);

In the RoleService class, only this row needs transactions:

saveData(userModel);

The current writing method will cause all query methods to be included in the same transaction.

If there are many query methods, the call level is very deep, and some query methods are time-consuming, the whole transaction will be very time-consuming, resulting in large transaction problems.

About the harm of big business problems, you can read another article of mine< How to solve the big headache problem? >, there are detailed explanations on it.

2. Programming services

The above content is based on @ Transactional annotation, mainly about its transaction problem. We call this kind of transaction: declarative transaction.

In fact, spring also provides another way to create transactions, that is, transactions implemented by manually writing code. We call this kind of transaction: programming transaction. For example:

   @Autowired
   private TransactionTemplate transactionTemplate;
   
   ...
   
   public void save(final User user) {
         queryData1();
         queryData2();
         transactionTemplate.execute((status) => {
            addData1();
            updateData2();
            return Boolean.TRUE;
         })
   }

In spring, in order to support programmatic transactions, a special class is provided: TransactionTemplate, which implements the function of transactions in its execute method.

Compared with the @ Transactional annotation declarative transaction, I suggest you use the programmatic transaction based on the TransactionTemplate. The main reasons are as follows:

  1. Avoid transaction invalidation due to spring aop problems.

  2. It can control the scope of transactions with smaller granularity and is more intuitive.

It is recommended to use less @ Transactional annotation to start transactions in the project. However, it does not mean that it must not be used. If some business logic in the project is relatively simple and does not change frequently, it is OK to use @ Transactional annotation to start a transaction, because it is simpler and more efficient. However, be careful about transaction failure.

One last word (please pay attention, don't whore me in vain)

Topics: Java Spring Back-end