12 scenarios of Spring transaction failure

Original text: Talk about 12 scenarios of Spring transaction failure, it’s too bad-spring transaction failure situation (51cto.com)

12 scenarios of Spring transaction failure

foreword

A transaction does not take effect

1. Access rights issue

2. The method is modified with final

3. Method internal call

4. Not managed by spring

5. Multi-thread call

6. Tables do not support transactions

7. Unopened transaction

Two transactions do not roll back

1. Error propagation characteristics

2. Swallow the exception yourself

3. Manually throw other exceptions

4. Customized rollback exception

5. More nested transaction rollbacks

three other

1 big business problem

2. Programmatic transactions


Preface

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

In some business scenarios, if a request needs to write data from multiple tables at the same time. In order to ensure the atomicity of operations (either succeed at the same time or fail at the same time) and avoid data inconsistency, we generally use spring transactions.

Indeed, spring transactions are very cool to use, just use a simple annotation: @Transactional, you can easily handle transactions. I guess most of my friends also use it like this, and it’s been cool to use it all the time.

But if you use it incorrectly, it can also trap you invisible.

Today we will talk together, some scenarios where the transaction fails, maybe you have already been recruited. Don’t believe me, let’s take a look together.

A transaction does not take effect

1. Access permission problem

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

However, if we define wrong access permissions for certain transaction methods during the development process, it will cause problems with the transaction function, 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 cause the transaction to fail, and spring requires that the proxy method must be public.

To put it bluntly, there is a judgment in the computeTransactionAttribute method of the AbstractFallbackTransactionAttributeSource class. If the target method is not public, 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() & amp; & amp; !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 & amp; & amp; 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 & amp; & amp; ClassUtils. isUserLevelMethod(method)) {
        return txAttr;
      }
    }
    return null;
  } 

That is to say, if our custom transaction method (that is, the target method) has access rights other than public, but private, default or protected, spring will not provide transaction functions.

2. The method is modified with final

Sometimes, a method does not want to be rewritten by subclasses, then the method can be defined as final. It is no problem to define ordinary methods like this, but if the transaction method is defined 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 cause the transaction to fail.

Why?

If you have read 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 implement transaction functions in proxy classes.

But if a method is modified with final, then in its proxy class, the method cannot be rewritten to add transaction functions.

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

3. Method internal call

Sometimes we need to call another transaction method in a method of a Service class, such as:

@Service
public class UserService {
 
    @Autowired
    private UserMapper userMapper;
 
    @Transactional
    public void add(UserModel userModel) {
        userMapper.insertUser(userModel);
        updateStatus(userModel);
    }
 
    @Transactional
    public void updateStatus(UserModel userModel) {
        doSameThing();
    }
} 

We see that in the transaction method add, the transaction method updateStatus is directly called. From the content introduced above, we can know that the updateStatus method has the transaction ability because spring aop generates a proxy object, but this method directly calls the method of this object, so the updateStatus method will not generate a transaction.

It can be seen that direct internal calls of methods in the same class will cause the transaction to fail.

So the question is, if in some scenarios, you really want to call another method of itself in a method of the same class, what should you do?

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 requires transaction execution to the new method. The specific code is 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 code is 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 there be a circular dependency problem with this approach?

Answer: no.

In fact, the three-level cache inside spring ioc guarantees it, and there will be no circular dependency problem.

3.3 Through the AopContent class

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

The above method 2 can indeed solve the problem, but the code does not look intuitive, and the same function can be achieved by using AOPProxy in the Service class to obtain the proxy object. 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 usual development process, there is a detail that is easily overlooked. That is, the premise of using spring transactions is: the object needs to be managed by spring, and a bean instance needs to be created.

Usually, we can automatically realize the functions of bean instantiation and dependency injection through @Controller, @Service, @Component, @Repository and other annotations.

If one day, you developed a Service class in a hurry, but forgot to add the @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 the UserService class is not annotated with @Service, then the class will not be handed over to spring management, so its add method will not generate transactions.

5. Multi-thread call

In actual project development, there are quite a lot of multi-threaded usage scenarios. Will there be any problems if spring transactions are used in multi-threaded scenarios?

@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("save role table data");
    }
} 

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

This will cause the two methods to not be in the same thread, and the obtained database connections will be different, thus 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 connections. A map is saved in the current thread, the key is the data source, and the value is the database connection.

private static final ThreadLocal<Map<Object, Object>> resources =
 
  new NamedThreadLocal<>("Transactional resources"); 

The same transaction we are talking about actually refers to the same database connection. Only the same database connection can be committed and rolled back at the same time. If it is in different threads, the database connection obtained must be different, so it is a different transaction.

6. The table does not support transactions

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

Its benefits needless to say: index files and data files are stored separately, and for single-table operations with more queries and fewer writes, the performance is better than InnoDB.

In some old projects, it may still be used.

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 very fatal problem: it does not support transactions.

If it’s just a single table operation, it’s fine, and there won’t be too many problems. But if you need to operate across multiple tables, because it does not support transactions, the data is likely to be incomplete.

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

So in actual business scenarios, myisam is not used much. After mysql5, myisam has gradually withdrawn from the stage of history, replaced by innodb.

Sometimes during the development process, we find that the transaction of a certain table has not taken effect. It is not necessarily the fault of the spring transaction. It is best to confirm whether the table you are using supports transactions.

7. Unopened transaction

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

You may find this sentence funny.

Isn’t opening a transaction the most basic function in a project?

Why is the transaction still not opened?

That’s right, if the project has been built, there must be transaction functions.

But if you are building a project demo, there is only one table, and the transaction of this table does not take effect. So what could be the cause?

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

If you are using springboot project then you are in luck. Because springboot has silently opened the transaction for you through the DataSourceTransactionManagerAutoConfiguration class.

What you have to do is very simple, you only need to configure spring.datasource related parameters.

But 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 to configure, the transaction will definitely 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>
<!-- Use cut points to cut things in -->
<aop:config>
    <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/>
    <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
</aop:config> 

Silently speaking, if the entry point matching rules in the pointcut tag are incorrectly matched, some types of transactions will not take effect.

Second transaction does not roll back

1. Error propagation characteristics

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

The function of this parameter is to specify the propagation characteristics of the transaction. Spring currently supports 7 propagation characteristics:

  • REQUIRED If there is a transaction in the current context, then 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 supports the transaction to join the transaction. If there is no transaction, it executes in a non-transactional manner.
  • MANDATORY if there is a transaction in the current context, otherwise throw an exception.
  • REQUIRES_NEW creates a new transaction every time, and at the same time suspends the transaction in the context. After the execution of the current new transaction is completed, the context transaction resumes and executes again.
  • NOT_SUPPORTED If there is a transaction in the current context, the current transaction is suspended, and then the new method is executed in an environment without a transaction.
  • NEVER throws an exception if there is a transaction in the current context, otherwise code is executed on a no-transactional environment.
  • NESTED If a transaction exists in the current context, the nested transaction is executed, and if there is no transaction, a new transaction is created.

If we set the propagation characteristics wrongly when we manually set the propagation parameters, 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, and an exception will be thrown if there is a transaction.

Currently only these three propagation characteristics will create new transactions: REQUIRED, REQUIRES_NEW, NESTED.

2. I swallowed the exception myself

The transaction will not be rolled back. The most common problem is that the developer manually try…catch the exception 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, the spring transaction will of course not be rolled back, because the developer caught the exception himself and did not throw it manually, in other words, he swallowed the exception.

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

3. Manually throw other exceptions

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

@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 situation, the developer caught the exception himself, and then manually threw the exception: Exception, and the transaction will not be rolled back.

Because of the spring transaction, 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 using the @Transactional annotation to declare a transaction, sometimes we want to customize the exception of the rollback, and spring also supports it. This function can be accomplished by setting the rollbackFor parameter.

But if the value of this parameter is set wrong, it will lead to some inexplicable problems, such as:

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

If the above code is executed, when saving and updating data, the program reports an error and throws SqlException, DuplicateKeyException and other exceptions. And BusinessException is our custom exception, and the exception reported does not belong to BusinessException, so the transaction will not be rolled back.

Even though rollbackFor has a default value, developers are still required to re-specify this parameter in the Alibaba developer specification.

Why is this?

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 under normal circumstances.

5. There are 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("save role table data");
    }
} 

In this case, a nested internal transaction is used. Originally, when an exception occurs when calling the roleService.doOtherThing method, only the content in the doOtherThing method will be rolled back, and the content in userMapper.insertUser will not be rolled back, that is, the savepoint will be rolled back . . But the thing is, insertUser is also rolled back.

why?

Because there is an exception in the doOtherThing method, it will continue to be thrown up without manual capture, and the exception will be caught 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 just roll back the savepoint?

@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 the inner nested transaction in try/catch, and don’t continue to throw exceptions up. This ensures that if an exception occurs in the inner nested transaction, only the inner transaction is rolled back without affecting the outer transaction.

Three Others

1 big business problem

When using spring transactions, there is a very troublesome problem, which is the problem of large transactions.

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);
    }
} 

But the @Transactional annotation, if it is added to the method, has a disadvantage that the entire method is included in the transaction.

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

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

In the RoleService class, only this line requires a transaction:

saveData(userModel); 

The current way of writing 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 entire transaction will be very time-consuming, resulting in large transaction problems.

2. Programmatic transaction

The content discussed above is based on the @Transactional annotation, mainly talking about its transaction issues. 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: programmatic transaction. For example:

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

In order to support programmatic transactions in spring, a class is specially provided: TransactionTemplate, in its execute method, the transaction function is realized.

Compared with @Transactional annotation declarative transactions, I recommend that you use programmatic transactions based on TransactionTemplate. The main reasons are as follows:

  • Avoid the problem of transaction failure due to spring aop problems.
  • It is more intuitive to control the scope of transactions at a finer granularity.

It is recommended to use less @Transactional annotations in projects to start transactions. But it doesn’t mean that it must not be used. If some business logic in the project is relatively simple and does not change frequently, it is okay to use the @Transactional annotation to open the transaction to open the transaction, because it is simpler and more efficient in development, but be careful about the transaction failure problem.