Small knowledge, big challenge! This paper is participating in theEssentials for programmers”Creative activities.

Don’t answer that before you finish reading it. We all know that in Spring projects we can use the @Transactional annotation directly to identify a Transactional method. However, you may not know if the transaction is performing the way you want it to. Let’s take a look at a few situations where what you think transaction management is probably not what you think transaction management is.

0 Classic error cases

    @Transactional
    void transfer() {
        try{
            //...
        } catch (Exception e){
            LOGGER.error(e.getMessage())
        }
    }
Copy the code

1 Throw a check exception

If a method throws a check exception such as fileNotFound, the transaction will not be rolled back for the simple reason that the @Transactional annotation default rollbackFor is a runtime exception. This is why Ali’s development specification requires rollbackFor to be specified.

So when we use this annotation, we recommend that we specify rollbackFor so that you know exactly what the exception is that will rollback.

@Transactional(rollbackFor = Exception.class)
Copy the code

2 Incorrect try catch is used

The business method uses a try catch to catch an exception, and then the exception happens, and the result goes into your catch block, and you don’t throw it, and the transaction doesn’t get rolled back correctly. The principle here is that Spring uses a proxy for transaction management. The order of invocation is to start the transaction, execute the target method, commit or roll back the transaction. Although your target method fails and you cannot handle it yourself, the target method does not throw an exception from the perspective of the proxy class. So the transaction commits normally.

@Transactional(rollbackFor = Exception.class) public void transfer(int amount) { try{ serviceB.sub(amount); serviceA.add(amount); } catch (Exception e) { LOGGER.error("error occur"); }}Copy the code

There are two correct ways to do this: one is to continue throwing an exception in a catch, and the other is to tell Spring that my current transaction needs to rollback.

// 1
throw new RuntimeException(e);
// 2
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
Copy the code

Error adding section

AOP section of the order to the right of the transaction not rolled back, the reason is that the transaction slice lowest priority, but if the custom section and its priority, is still a custom section inner, at this moment we do not have the right to throw an exception if the custom section, eat in the catch the exception, at this time will appear similar to the second case, The proxy class does not get the exception information and does not roll back.

The solution is to throw an exception in the section as well, or to set the priority of the custom section to lower. It is recommended to use the first aspect to throw an exception, but then again, why would we add a aspect to a method that already has transaction management… Transaction methods are usually in the Service layer, and we can add facets to the Controller layer.

Transactional transactions can only be applied to public methods by default

Non-public methods cause transactions to fail. Spring creates a proxy for a method and adds transaction notifications, provided that the method is public. It is important to either set the method to public or set it to add transaction notifications for non-public methods.

It is recommended to use the public method rather than add configuration

    @Bean
    public TransactionAttributeSource transactionAttributeSource(){
        return new AnnotationTransactionAttributeSource(false);
    }
Copy the code

5 The propagation behavior becomes invalid when a method of this class is invoked

This problem occurs when two methods of the same Service are called between. Again, in the case of proxy objects, we expect the call to be a call from a proxy class, but if we call directly from within a method, unfortunately, the transaction invalidation of the called method is not enhanced by AOP.

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES)
    public void a (){
        b();
    }
​
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public void b (){
        
    }
Copy the code

The solution to improvement is to call yourself and inject yourself. You may have seen code like this, designed to solve this problem. This solution is the most common.

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES)
    public void a (){
        service.b();
    }
Copy the code

There is another way to get the proxy object through AopContext and call it. Note here that using this approach requires the exposure agent to be turned on.

@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {}
​
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void a (){
    ((TestService)AopContext.currentProxy()).b();
}
Copy the code

The point is there’s a comment here that says there’s no guarantee that the AopContext will work, uh uh well, inject itself.

/** * Indicate that the proxy should be exposed by the AOP framework as a {@code ThreadLocal} * for retrieval via the {@link org.springframework.aop.framework.AopContext} class. * Off by default, * @since 4.3.1 */ Boolean exposeProxy() default false;Copy the code

Transactional does not guarantee atomic behavior

This is a very common problem. We tend to think that adding transaction management, especially Propagation.REQUIRES_NEW, means that our transaction will commit after method execution, or synchronized is an atomic operation. Why is that? Start with the proxy class, which starts the transaction, executes the target method, and commits the transaction. Both the REQUIRES_NEW and synchronized keywords apply only to the target method, and even if the target method succeeds, the transaction is still uncommitted.

This is atomic for INSERT, DELETE, update, select — for update statements. But select is not.

The solution is to expand the scope of synchronized and lock the entire proxy method rather than the target method. You can also use SQL to control the atomicity of the operation, using select — for update