Have you stepped on these pits of failure

Keywords: Java Spring Transaction

In normal development, you will encounter the need to add transactions. The annotation @ Transactional is often used, but sometimes it is found that it is added explicitly, but it does not take effect. Why?

Today, let's talk about several transaction failure scenarios that we usually encounter to avoid stepping on the pit in the future. Click to see and learn, hahaha.

1, Transaction failure scenario

1. The access type of the method is not public

    @Transactional
    private void save() {
        User user = getUser();
        userMapper.insert(user);
        this.teachSave();
    }

We can see that the save method uses private, which is not easy to find.

spring requires that the proxy method must be public modified. It can be found from the following source code that it is required to be public, not return null in public, that is, it does not support transactions.

2. Method internal call

For example, there are two methods, test1 and test2. When test1 calls test2, there are four scenarios:

  • test1 has a transaction, test2 has no transaction, and the transaction takes effect

  • test1 has a transaction, test2 has a transaction, and the transaction takes effect

  • test1 has no transaction, test2 has a transaction, and the transaction does not take effect

  • test1 has no transaction, test2 has no transaction, and the transaction does not take effect

2.1 test1 has transactions, test2 has no transactions

    @Transactional
    public void test1() {
        User user = getUser();
        userMapper.insert(user);
        this.test2();
    }

    public void test2() {
        Teacher t = getTeacher();
        teacherMapper.insert(t);
        int a = 1 / 0;
    }

When the transaction of test1 method takes effect, the insertion of user and teacher tables belongs to the same transaction, either successful or failed.

2.2. test1 has transactions and test2 has transactions

    @Transactional
    public void test1() {
        User user = getUser();
        userMapper.insert(user);
        this.test2();
    }
    @Transactional
    public void test2() {
        Teacher t = getTeacher();
        teacherMapper.insert(t);
        int a = 1 / 0;
    }

When the transaction of test1 method takes effect, the transaction generation of test2 method does not take effect for the insertion of user and teacher tables. The results are the same. The effect is the same as 2.1. They belong to the same transaction, either successful or failed.

2.3. test1 has no transaction and test2 has transaction

    public void test1() {
        User user = getUser();
        userMapper.insert(user);
        this.test2();
    }

    @Transactional
    public void test2() {
        Teacher t = getTeacher();
        teacherMapper.insert(t);
        int a = 1 / 0;
    }

The user and teacher tables are inserted by directly calling the method of this object, so the transaction of test2 method is invalid and cannot be called by this.

2.4. test1 has no transaction and test2 has no transaction

    public void test1() {
        User user = getUser();
        userMapper.insert(user);
        this.test2();
    }

    public void test2() {
        Teacher t = getTeacher();
        teacherMapper.insert(t);
        int a = 1 / 0;
    }

This scenario is easy to understand. There are no transactions, so there are no transactions for user and teacher operations.

Through the above four scenarios, we can know that calling internal methods through this does not make the transaction effective. How can we make the transaction effective?

  • Put the method test2 in another serviceB and call test2 through serviceB, so that the transaction can take effect
@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceB serviceB;

    public void test1() {
        serviceB.test2();
    }

 @Servcie
 public class ServiceB {

    @Transactional(rollbackFor=Exception.class)
    public void test2() {
        addUser();
        addTeacher();
    }

 }

  • Inject yourself
@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceA serviceA;

    public void test1() {
        serviceA.test2();
    }
    @Transactional(rollbackFor=Exception.class)
    public void test2() {
        addUser();
        addTeacher();
    }
}

Inject yourself. By calling test2, the transaction takes effect

  • Get the proxy object using AopContext.currentProxy()
@Servcie
public class ServiceA {
  public void test1() {
        ((ServiceA)AopContext.currentProxy()).test2();
    }
    @Transactional(rollbackFor=Exception.class)
    public void test2() {
        addUser();
        addTeacher();
    }
}

Use AopContext.currentProxy() in service class A to obtain the proxy object, and the transaction takes effect.

3. Exception type error

@Service
public class ServiceA {

    @Transactional
    public void add(UserModel userModel) throws Exception {
        try {
             addUser();
             addTeacher();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }
}

You can see that when an Exception occurs, it is supplemented by try catch and an Exception is thrown. However, because of spring transactions, only RuntimeException (runtime Exception) and Error (Error) will be rolled back by default. For ordinary exceptions (non runtime exceptions), it will not be rolled back. Therefore, transaction is generally required to specify rollback for.

4. The exception thrown was eaten

@Service
public class ServiceA {

    @Transactional(rollbackFor=Exception.class)
    public void add(UserModel userModel) throws Exception {
        try {
             addUser();
             addTeacher();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

The above exception was eaten by try catch, and no exception was thrown out, so the transaction cannot be rolled back at this time.

5. The propagation parameter of @ Transactional is not set correctly

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 join 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 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 will be executed. If there is no transaction, a new transaction will be created.

For example, the following code is set to never, which will throw an exception when there is a transaction

    @Transactional(propagation=Propagation.NEVER)
    public void test2() {
        addUser();
        addTeacher();
    }

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

2, Transaction submission method

1. Declarative transaction - Auto commit

   @Transactional(rollbackFor=Exception.class)
   public void save(final User user) {
         queryUser();
         queryTeacher();
         addUser();
         addTeacher();
   }

The annotation Transactional is used to wrap the whole method in transactions, but the query query method does not need transactions. Only addUser and addTeacher really need transactions, resulting in large transactions.

2. Programming transactions - Manual commit

   public void save(final User user) {
         queryUser();
         queryTeacher();
         transactionTemplate.execute((status) => {
            addUser();
            addTeacher();
            return Boolean.TRUE;
         })

Where transactions need to be committed, commit manually to narrow the scope of transactions.

Therefore, it is recommended to use programmatic transactions to control the scope of transactions with smaller granularity.

Pay attention to the official account: to help the shrimp and share more java backend technology dry cargo

Posted by jason_kraft on Mon, 27 Sep 2021 17:32:11 -0700