Common scenarios of Spring transaction failure

Posted by CentralOGN on Wed, 19 Jan 2022 21:14:10 +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 rights in the development process, it will lead to problems in transaction functions, such as:

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

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;\
  }
Copy code

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

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;

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

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

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

 }
Copy code

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();
    }
 }
Copy code

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 know more about circular dependency, you can see my previous article< spring: how do I solve circular dependencies?>.

3.3 passing AopContent class

Use aopcontext. In this Service class Currentproxy() gets the proxy object

The above method 2 can indeed solve the problem, but the code does not look intuitive. You can also use AOPProxy to obtain the proxy object in the Service class to achieve the same function. The specific codes are 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();
    }
 }
Copy code

4. Not managed by spring

In our usual development process, one detail is easy to be ignored. That is, the premise of using spring transactions is: to be managed by spring, you need to create bean instances.

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

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. Multithreaded call

In actual project development, there are many scenarios for multithreading. Is there a problem if spring transactions are used in a multithreaded scenario?

@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");
    }
}
Copy code

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.

This will result in two methods not in the same thread and different database connections obtained, but two different transactions. If you want to throw an exception in the doOtherThing method, it is impossible to roll back the add method.

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. key is the data source and value is the database connection.

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

  new NamedThreadLocal<>("Transactional resources");
Copy code

When we talk about the same transaction, we actually mean the same database connection. Only with the same database connection can we commit and rollback at the same time. If you are in different threads, the database connections you get must be different, so 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
 Copy code

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 spring Datasource related parameters are sufficient.

However, if you are still using a traditional spring project, you need to use ApplicationContext In the XML file, manually configure transaction related parameters. 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> 
Copy code

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, the transaction can be added to the transaction. If there is no transaction, it can be executed in a non transaction manner.
  • MANDATORY = if there is a transaction in the current context, otherwise an exception is thrown.
  • REQUIRES_NEW} will create a new transaction each time and suspend the transaction in the context. After the current new transaction is completed, the context transaction will be resumed and executed again.
  • NOT_SUPPORTED if there is a transaction in the current context, suspend the current transaction, and then the new method is executed in an environment without a transaction.
  • NEVER: if there is a transaction in the current context, an exception will be thrown; otherwise, the code will be executed in a transaction free environment.
  • NESTED} if there is a transaction in the current context, the NESTED transaction will be executed. If there is no transaction, a new transaction will be 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);
    }
}
Copy code

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 in the code Catch exception. 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);
        }
    }
}
Copy code

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

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

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");
    }
}
Copy code

In this case, nested internal transactions are used, originally hoping to call roleservice If an exception occurs in the doOtherThing method, only the contents in the doOtherThing method are rolled back, not usermapper Contents in insertUser, that is, rollback savepoint.. 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);
        }
    }
}
Copy code

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

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);
Copy code

In the RoleService class, only this row needs transactions:

saveData(userModel);
Copy code

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 realized 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;
         })
   }
Copy code

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.

Topics: Java Spring Back-end Programmer Deep Learning