3
头图

background

In our work, we often use @Transactional to declare transactions. Incorrect usage will cause annotations to fail. Let’s analyze the four most common cases where @Transactional transactions do not take effect:

  • Access inside the class: the a1 method of class A is not marked with @Transactional, the a2 method is marked with @Transactional, and a2 is called in a1;
  • Private methods : Annotate @Transactional annotations on non-public methods;
  • Exception mismatch : @Transactional does not set the rollbackFor property, the method returns Exception and other exceptions;
  • Multi-threading : The main thread and the sub-thread are called, and the thread throws an exception.

sample code

UserDao interface, operate database; UserController implements business logic, declares transaction, calls UserController.testSuccess method, transaction declaration takes effect

 // 提供的接口
public interface UserDao {
    // select * from user_test where uid = "#{uid}"
    public MyUser selectUserById(Integer uid);
    // update user_test set uname =#{uname},usex = #{usex} where uid = #{uid}
    public int updateUser(MyUser user);
}
 @Service
public class UserController {
    @Autowired
    private UserDao userDao;

    public void update(Integer id) {
        MyUser user = new MyUser();
        user.setUid(id);
        user.setUname("张三-testing");
        user.setUsex("女");
        userDao.updateUser(user);
    }

    public MyUser query(Integer id) {
        MyUser user = userDao.selectUserById(id);
        return user;
    }

    // 正常情况
    @Transactional(rollbackFor = Exception.class)
    public void testSuccess() throws Exception {
        Integer id = 1;
        MyUser user = query(id);
        System.out.println("原记录:" + user);
        update(id);
        throw new Exception("事务生效");
    }
}
In order to quickly illustrate the problem, the business logic and transaction declaration are directly implemented in the controller, which does not represent the code layering in the production environment

1. Class Internal Access

Add a new method testInteralCall() to the class UserController:

 public void testInteralCall() throws Exception {
    testSuccess();
    throw new Exception("事务不生效:类内部访问");
}

Here testInteralCall() is not marked with @Transactional, let's look at the test case again:

 public static void main(String[] args) throws Exception {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserController uc = (UserController) applicationContext.getBean("userController");
    try {
        uc.testSuccess();
    } finally {
        MyUser user =  uc.query(1);
        System.out.println("修改后的记录:" + user);
    }
}
// 输出:
// 原记录:MyUser(uid=1, uname=张三, usex=女)
// 修改后的记录:MyUser(uid=1, uname=张三-testing, usex=女)

As you can see from the above output, the transaction has not been rolled back. What is the reason for this?

Because the working mechanism of @Transactional is based on AOP implementation, AOP is implemented using dynamic proxy. If testSuccess() is called directly through the proxy, it will be enhanced before and after through AOP. The enhanced logic is actually to add enable before and after testSuccess(). , The logic of committing the transaction.

Now, testSuccess() is called through testInteralCall(), and no enhancement operation will be performed before and after testSuccess(), that is, the internal call of the class will not be accessed by proxy.

2. Private methods

On private methods, adding the @Transactional annotation will not work either:

 @Transactional(rollbackFor = Exception.class)
private void testPirvateMethod() throws Exception {
    Integer id = 1;
    MyUser user = query(id);
    System.out.println("原记录:" + user);
    update(id);
    throw new Exception("测试事务生效");
}

When using it directly, the following scenario is not easy to appear, because IDEA will remind you that the text is: Methods annotated with '@Transactional' must be overridable. As for the in-depth principle, the source code part will explain it to you.

3. Exception mismatch

The @Transactional here doesn't set the rollbackFor = Exception.class property:

 @Transactional
public void testExceptionNotMatch() throws Exception {
    Integer id = 1;
    MyUser user = query(id);
    System.out.println("原记录:" + user);
    update(id);
    throw new Exception("事务不生效:异常不匹配");
}

The @Transactional annotation handles runtime exceptions by default, that is, only when a runtime exception is thrown, the transaction will be rolled back, otherwise it will not be rolled back. As for the in-depth principle, the source code will explain it to you.

4. Multithreading

The parent thread throws an exception

The parent thread throws an exception, the child thread does not throw an exception:

 public void testSuccess() throws Exception {
    Integer id = 1;
    MyUser user = query(id);
    System.out.println("原记录:" + user);
    update(id);
}
@Transactional(rollbackFor = Exception.class)
public void testMultThread() throws Exception {
    new Thread(new Runnable() {
        @SneakyThrows
        @Override
        public void run() {
            testSuccess();
        }
    }).start();
    throw new Exception("测试事务不生效");
}

The parent thread throws the thread and the transaction is rolled back, because the child thread exists independently and is not in the same transaction as the parent thread, so the modification of the child thread will not be rolled back

The child thread throws an exception

The parent thread does not throw an exception, the child thread throws an exception:

 public void testSuccess() throws Exception {
    Integer id = 1;
    MyUser user = query(id);
    System.out.println("原记录:" + user);
    update(id);
    throw new Exception("测试事务不生效");
}
@Transactional(rollbackFor = Exception.class)
public void testMultThread() throws Exception {
    new Thread(new Runnable() {
        @SneakyThrows
        @Override
        public void run() {
            testSuccess();
        }
    }).start();
}

Since the exception of the child thread will not be caught by the external thread, the parent thread does not throw an exception, and the transaction rollback does not take effect.

Source code interpretation

@Transactional execution mechanism

We only look at the core logic. The interceptorOrInterceptionAdvice in the code is the instance of TransactionInterceptor, and the input parameter is this object.

The red box has a comment that roughly translates to "It's an interceptor, so we just call: the pointcut will be computed statically before constructing this object."

transactional_1.png

this is the ReflectiveMethodInvocation object, and the member objects include UserController class, testSuccess() method, input parameters and proxy objects.

transactional_2.png

After entering the invoke() method:

transactional_3.png

High energy ahead! ! ! Here is the core logic of the transaction, including judging whether the transaction is opened, the target method is executed, the transaction is rolled back, and the transaction is committed.

transactional_4.png

private causes the transaction to not take effect

In the above picture, the method getTransactionAttribute() is called in the first red box area, mainly to obtain the txAttr variable, which is used to read the configuration of @Transactional. If this txAttr = null, the transaction will not follow Logic, let's look at the meaning of this variable:

transactional_5.png

We go straight to getTransactionAttribute(), focusing on the method to get the transaction configuration.

transactional_6.png

High energy ahead! ! ! This is the reason why the private transaction does not take effect . allowPublicMethodsOnly() always returns false, so the focus is only on the isPublic() method.

transactional_7.png

exception mismatch reason

We continue to go back to the core logic of the transaction, because the main method throws an Exception() exception and enters the logic of transaction rollback:

transactional_8.png

Enter the rollbackOn() method to determine whether the exception can be rolled back. This needs to determine whether the Exception() exception thrown by the main method is in the configuration of @Transactional:

transactional_9.png

Let's go to getDepth() to see the exception rule matching logic, because we configured rollbackFor = Exception.class for @Transactional, it can match successfully:

transactional_10.png

The winner in the example is not null, so the following link will be skipped. But when winner = null, that is, when the rollbackFor property is not set, the default exception capture method will be taken.

transactional_11.png

High energy ahead! ! ! Here is why the exception does not match , let's take a look at the default exception capture method:

transactional_12.png

Is it suddenly clear that when the rollbackFor attribute is not set, only the exceptions of RuntimeException and Error are rolled back by default.


skyarthur
1.6k 声望1.3k 粉丝

技术支持业务,技术增强业务,技术驱动业务