The introduction

The @Transactional annotation is a common annotation used in development to ensure that multiple database operations within a method will either succeed or fail at the same time. There are a lot of details to pay attention to when using the @Transactional annotation, or you’ll find that @Transactional always fails out of nowhere.

The transaction

Transaction management is an indispensable part of system development. Spring provides a good transaction management mechanism, which is mainly divided into programmatic transaction and declarative transaction.

Programmatic transaction

Refers to the manual management of transaction submission, rollback and other operations in the code, the code is highly intrusive, as shown in the following example:

try {
    //TODO something
     transactionManager.commit(status);
} catch (Exception e) {
    transactionManager.rollback(status);
    throw new InvoiceApplyException("Abnormal failure");
}
Copy the code

Declarative transaction

AOP based on the aspect, it decoupled the concrete business and transaction processing part, the code invasion is very low, so in the actual development of declarative transactions used more. Declarative transactions can also be implemented using XML configuration files based on TX and AOP, or using the @Transactional annotation.

    @Transactional
    @GetMapping("/test")
    public String test(a) {
        int insert = cityInfoDictMapper.insert(cityInfoDict);
    }
Copy the code

Where does the @Transactional annotation work?

Transactional can work on interfaces, classes, and class methods.

  • On the classThe @Transactional annotation, when placed ona class, represents all of the Transactional annotations of that classpublicMethods are configured with the same transaction property information.
  • Applied to methods: When a class is configured with @Transactional and a method is configured with @Transactional, the method’s transaction overrides the Transactional configuration information of the class.
  • Use on interfaces: This is not recommended because the @Transactional annotation will fail once annotation is on an Interface and Spring AOP is configured to use CGLib dynamic proxies
@Transactional
@RestController
@RequestMapping
public class MybatisPlusController {
    @Autowired
    private CityInfoDictMapper cityInfoDictMapper;
    
    @Transactional(rollbackFor = Exception.class)
    @GetMapping("/test")
    public String test(a) throws Exception {
        CityInfoDict cityInfoDict = new CityInfoDict();
        cityInfoDict.setParentCityId(2);
        cityInfoDict.setCityName("2");
        cityInfoDict.setCityLevel("2");
        cityInfoDict.setCityCode("2");
        int insert = cityInfoDictMapper.insert(cityInfoDict);
        return insert + ""; }}Copy the code

The Transactional transaction property

The propagation properties

Propagation Indicates the propagation behavior of the transaction. The default value is Propagation.REQUIRED.

  • Propagation.REQUIRED: If a transaction currently exists, join the transaction. If no transaction currently exists, create a new transaction. (That is, if methods A and B are annotated, the default propagation mode will merge the two methods’ transactions into one if method A calls method B internally.)
  • Propagation.SUPPORTS: If a transaction exists, join the transaction. If no transaction currently exists, it continues in a non-transactional manner.
  • Propagation.MANDATORY: If a transaction exists, join the transaction. If no transaction currently exists, an exception is thrown.
  • Propagation.REQUIRES_NEW: Creates a new transaction and suspends the current transaction if one exists.(When A method in class A uses the defaultPropagation.REQUIREDPattern, B method of class B plus adoptionPropagation.REQUIRES_NEWSchema, and then call method B in method A to operate on the database, but after method A throws an exception, method B does not roll back, becausePropagation.REQUIRES_NEWSuspends the transaction for method A)
  • Propagation.NOT_SUPPORTED: Runs in a non-transactional manner, suspending the current transaction if one exists.
  • Propagation.NEVER: Runs in a non-transactional manner, throwing an exception if a transaction currently exists.
  • Propagation.NESTEDPropagation.

The isolation properties

Isolation: Isolation level of a transaction. The DEFAULT is Isolation.default.

  • Isolation.DEFAULT: Use the DEFAULT Isolation level of the underlying database.
  • Isolation.READ_UNCOMMITTED
  • Isolation.READ_COMMITTED
  • Isolation.REPEATABLE_READ
  • Isolation.SERIALIZABLE

The timeout attribute

Timeout: indicates the timeout period of a transaction. The default value is -1. If the time limit is exceeded but the transaction has not completed, the transaction is automatically rolled back.

ReadOnly attribute

ReadOnly: specifies whether the transaction is read-only. The default value is false. To ignore methods that do not require transactions, such as reading data, you can set read-only to true.

RollbackFor properties

RollbackFor: Specifies the type of exception that can trigger a transaction rollback. Multiple exception types can be specified.

NoRollbackFor properties

NoRollbackFor: Throws the specified exception type, does not roll back the transaction, or can specify multiple exception types.

Declarative transaction failure scenarios:

  1. annotations@TransactionalConfigured methods are not public permission modifiers;
  2. annotations@TransactionalBeans whose class is not Spring container-managed;
  3. annotations@TransactionalIn the class, methods modified by annotations are called by methods inside the class.
  4. The business code throws an exception of type notRuntimeException, transaction failure;
  5. If there is an exception in the business codeThe try... The catch...Statement block capture, whilecatchStatement block does not havethrow new RuntimeExecptionThe exception; (The most difficult problem to be identified and easily ignored)
  6. annotations@TransactionalIn thePropagationProperty value is incorrectly setPropagation.NOT_SUPPORTED(This propagation mechanism is generally not set)
  7. If mysql is a relational database and the storage engine is MyISAM instead of InnoDB, transactions will not work (not encountered in basic development); Based on the above scenario, Xiyuan will give a detailed explanation to your friends.

Note: Spring transactions are rolled back only when runtimeExceptions and errors occur in the program.

Non-public permission modifier

Refer to the official Spring documentation, the abstract and translation are as follows:

When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected.private or package-
visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-publicWhen using proxies, you should only apply the @Transactional annotation to methods that have public visibility. An error is not raised if a protected, private, or package-visible method is annotated using the @Transactional annotation, but the annotated method does not show configured transaction Settings. If you need to annotate non-public methods, consider using AspectJ (see below).Copy the code

It fails because in Spring AOP proxying, as shown above, the TransactionInterceptor intercepts the target method before and after execution. DynamicAdvisedInterceptor (CglibAopProxy inner classes) intercept method or JdkDynamicAopProxy invoke method indirect invocation AbstractFallbackTransactionAttributeSource computeTransactionAttribute method, obtain Transactional annotation of transaction configuration information.

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

This method checks if the target method’s modifier is public. Otherwise, it does not get the @Transactional attribute configuration information. Transactional can only be used with public methods, otherwise transactions will not fail. To use a non-public method, enable the AspectJ proxy mode. Currently, the compiler also gives an obvious hint if the @Transactional annotation works ona non-public method

Non-spring container-managed beans

Based on this failure scenario, experienced bosses are basically immune to such errors; The @Service annotation annotation, the StudentServiceImpl class is not managed by the Spring container, so even if a method is decorated with the @Transactional annotation, the transaction does not take effect.

A simple example is as follows:


//@Service 
public class StudentServiceImpl implements StudentService {

    @Autowired
    private StudentMapper studentMapper;

    @Autowired
    private ClassService classService;

    @Override
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void insertClassByException(StudentDo studentDo) throws CustomException {
        studentMapper.insertStudent(studentDo);
        throw newCustomException(); }}Copy the code

Methods modified by annotations are called by methods inside the class

Note: annotations do not take effect if a method calls another method in the same class with an annotation (e.g. @async, @transational).

This failure scenario is one of the most common pitfalls in our daily development; There is method in class A and method b, then the method b in @ Transactional affairs with the method level, inside the method A call the method b, method b inside transaction will not take effect.

The reason: Spring automatically generates a proxy class for a class annotated with the @Transactional annotation when scanning beans. When an annotated method is called, it is actually called by the proxy class. The proxy class starts a transaction before invoking it, but methods in the class call each other. Equivalent to this.b (), the B method is not called by the proxy class, but directly from the original Bean, so the annotation is invalid.

Case 1:

The following code has two methods, one with the @transational annotation and one without. If the annotated addPerson() method is called, a Transaction is initiated; If updatePersonByPhoneNo() is called because it internally calls addPerson() with annotations, the system will not start a Transaction for it.

@Service
public class PersonServiceImpl implements PersonService {
 
 @Autowired
 PersonDao personDao;
 
 @Override
 @Transactional
 public boolean addPerson(Person person) {
  boolean result = personDao.insertPerson(person)>0 ? true : false;
  return result;
 }
 
 @Override
 //@Transactional
 public boolean updatePersonByPhoneNo(Person person) {
  boolean result = personDao.updatePersonByPhoneNo(person)>0 ? true : false;
  addPerson(person); // Test whether @transactional works in the same class
  returnresult; }}Copy the code

Why is it that when a method () calls another method b() in the same class, b() is not called through a proxy class? Take a look at the following example (in pseudocode for simplicity) :

@Service
class A{
    @Transactinal
    method b(a){... }method a(a){    / / mark 1b(); }}Copy the code
//Spring scans the annotations, creates another proxy class, and inserts a startTransaction() method for the annotated method:
class proxy$A{
    A objectA = new A();
    method b(a){    / / mark 2
        startTransaction();
        objectA.b();
    }
 
    method a(a){    / / mark 3
        objectA.a();    // Since a() has no annotations, transaction is not initiated and the a() method of an instance of A is called directly}}Copy the code

When we call A () on A’s bean, we are intercepted by Proxy A, executing proxyA, and proxya.a () (flag 3). However, we know from the above code that objecta.a () is called. The a() method is called by the original bean, so the code goes to “tag 1”. Thus, “Flag 2” is not executed, so the startTransaction() method is not running.

Conclusion: Within a Service, nested calls between transactional methods, or between ordinary methods and transactional methods, do not start a new transaction.

  1. Spring uses a dynamic proxy mechanism for transaction control, and the dynamic proxy eventually calls the original object, which does not fire the proxy when it calls a method!

  2. Spring’s Transactional management is implemented using AOP. Its IMPLEMENTATION for non-final classes is cglib, which generates a subclass of the current class as a proxy class and then determines whether that method has the @Transactional annotation when it calls a method under it. Transaction management (intercepting method calls, executing transactions, and so on) is implemented through dynamic proxies. When a() is called from B (), the @Transactional annotation is not found on B (), so the whole AOP proxy process (transaction management) does not occur.

Method call execution flow after AOP proxy:

Case 2

@Service
public class ClassServiceImpl implements ClassService {

    @Autowired
    private ClassMapper classMapper;

    public void insertClass(ClassDo classDo) throws CustomException {
        insertClassByException(classDo);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void insertClassByException(ClassDo classDo) throws CustomException {
        classMapper.insertClass(classDo);
        throw newRuntimeException(); }}// Test case:
@Test
    public void insertInnerExceptionTest(a) throws CustomException {
       classDo.setClassId(2);
       classDo.setClassName("java_2");
       classDo.setClassNo("java_2");

       classService.insertClass(classDo);
    }
Copy the code

// Test result:

java.lang.RuntimeException
 at com.qxy.common.service.impl.ClassServiceImpl.insertClassByException(ClassServiceImpl.java:34)
 at com.qxy.common.service.impl.ClassServiceImpl.insertClass(ClassServiceImpl.java:27)
 at com.qxy.common.service.impl.ClassServiceImpl$$FastClassBySpringCGLIB$$a1c03d8.invoke(<generated>)
Copy the code

Although the business code reported an error, data was successfully inserted into the database and the transaction did not take effect;

Solution: a class uses its proxy class internally to invoke transaction methods,

public void insertClass(ClassDo classDo) throws CustomException {
        //insertClassByException(classDo);((ClassServiceImpl)AopContext.currentProxy()).insertClassByException(classDo); } Test cases:@Test
    public void insertInnerExceptionTest(a) throws CustomException {
       classDo.setClassId(3);
       classDo.setClassName("java_3");
       classDo.setClassNo("java_3");

       classService.insertClass(classDo);
    }
Copy the code

The business code throws an exception, the database does not insert new data, to achieve our purpose, successfully solve a transaction failure problem;

The database data has not changed;

Note: It is important to note that the @enableAspectJAutoProxy (exposeProxy = true) annotation should be added to the startup class, otherwise the startup fails:

java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.

 at org.springframework.aop.framework.AopContext.currentProxy(AopContext.java:69)
 at com.qxy.common.service.impl.ClassServiceImpl.insertClass(ClassServiceImpl.java:28)
Copy the code

The exception type is not RuntimeException

RollbackFor specifies the type of exception that can trigger a transaction rollback. By default, Spring throws unchecked exceptions (exceptions inherited from RuntimeException) or errors to roll back a transaction; Other exceptions do not trigger rollback transactions. If other types of exceptions are thrown in a transaction, but Spring is expected to rollback the transaction, you need to specify the rollbackFor property.

// We want the custom exception to be rolled back,
// If the exception thrown in the target method is a subclass of the exception specified by 'rollbackFor', the transaction will also be rolled back.
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class)
Copy the code

case

@Service
public class ClassServiceImpl implements ClassService {

    @Autowired
    private ClassMapper classMapper;

    //@Override
    //@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
    public void insertClass(ClassDo classDo) throws Exception {
        // Even though the internal transaction method is invoked using a proxy object here, the data is not rolled back and the transaction mechanism is invalidated
        ((ClassServiceImpl)AopContext.currentProxy()).insertClassByException(classDo);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void insertClassByException(ClassDo classDo) throws Exception {
        classMapper.insertClass(classDo);
        Throws a non-runtimeException type
        throw newException(); } Test cases:@Test
    public void insertInnerExceptionTest(a) throws Exception {
       classDo.setClassId(3);
       classDo.setClassName("java_3");
       classDo.setClassNo("java_3"); classService.insertClass(classDo); }}Copy the code

Results: The business code throws an exception, but the database is updated;

java.lang.Exception
 at com.qxy.common.service.impl.ClassServiceImpl.insertClassByException(ClassServiceImpl.java:35)
 at com.qxy.common.service.impl.ClassServiceImpl$$FastClassBySpringCGLIB$$a1c03d8.invoke(<generated>)
 at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
Copy the code

The database still inserts data, which is not what we want.

Solution:

The Transactional annotation decorates a method with the rollbackfor attribute value that specifies the rollback exception type: @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)

@Override
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void insertClassByException(ClassDo classDo) throws Exception {
        classMapper.insertClass(classDo);
        throw new Exception();
    }
Copy the code

After an exception is caught, no exception is thrown

Using a try-catch in a transaction method so that an exception cannot be thrown will naturally invalidate the transaction.

    @Transactional
    private Integer A(a) throws Exception {
        int insert = 0;
        try {
            CityInfoDict cityInfoDict = new CityInfoDict();
            cityInfoDict.setCityName("2");
            cityInfoDict.setParentCityId(2);
            /** * A inserts data with field 2 */
            insert = cityInfoDictMapper.insert(cityInfoDict);
            /** * B Inserts data with field 3 */
            b.insertB();
        } catch(Exception e) { e.printStackTrace(); }}Copy the code

If B throws an exception internally and A tries B’s exception, the transaction cannot be rolled back properly. Will throw an exception:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
Copy the code

Because when an exception is thrown in ServiceB, ServiceB indicates that the current transaction needs to rollback. But in ServiceA, because you manually catch the exception and handle it, the ServiceA assumes that the current transaction should be committed. It will appear inconsistent, that is, because of this, throw the front UnexpectedRollbackException anomalies.

Spring transactions are started before the business methods are called, and commit or rollback is performed after the business methods are executed, depending on whether a Runtime exception is thrown. If a Runtime exception is thrown and there is no catch in your business method, the transaction will be rolled back.

Transactional(rollbackFor= exception.class) ¶ A Transactional(rollbackFor= exception.class) ¶ A Transactional(rollbackFor= exception.class) ¶ Otherwise, the transaction will fail, and data commit will cause data inconsistency.

case

@Service
public class ClassServiceImpl implements ClassService {

    @Autowired
    private ClassMapper classMapper;

    //@Override
    public void insertClass(ClassDo classDo) {
        ((ClassServiceImpl)AopContext.currentProxy()).insertClassByException(classDo);

    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void insertClassByException(ClassDo classDo) {
        classMapper.insertClass(classDo);
        try {
            int i = 1 / 0;
        } catch(Exception e) { e.printStackTrace(); }} test cases:@Test
    public void insertInnerExceptionTest(a) {
       classDo.setClassId(4);
       classDo.setClassName("java_4");
       classDo.setClassNo("java_4");

       classService.insertClass(classDo);
    }
Copy the code

Execution Result:

Solution: Catch the exception and throw it

 @Override
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void insertClassByException(ClassDo classDo) {
        classMapper.insertClass(classDo);
        try {
            int i = 1 / 0;
        } catch (Exception e) {
            e.printStackTrace();
            throw newRuntimeException(); }}Copy the code

The transaction propagation behavior setting is abnormal

Propagation.NOT_SUPPORTED is not supported

 @Transactional(propagation = Propagation.NOT_SUPPORTED,rollbackFor = Exception.class)
    public void insertClassByException(ClassDo classDo) {
        classMapper.insertClass(classDo);
        try {
            int i = 1 / 0;
        } catch (Exception e) {
            e.printStackTrace();
            throw newRuntimeException(); }}Copy the code

The database storage engine does not support transactions

For example, if the storage engine of MySQL relational data is set to MyISAM, the transaction fails because the MyISMA engine does not support transaction operations.

Therefore, for transactions to take effect, you need to set the storage engine to InnoDB. The default storage engine of MySQL from 5.5.5 is InnoDB.

Do Spring transaction management in a paralletStream? (understand)

Only the main thread of the request is managed by the transaction; the other threads are not managed by the transaction.

Cause: The SqlSession is different.