Where does one @Transaction come from so many pits?

table of Contents

Preface
Affairs failed
database level
business code level
summary
transaction rollback issues related to
problems with using separate read and write transactions combined
summary

Preface

In the previous article, we have done a detailed analysis of the transaction in Spring. In this article, let’s talk about some problems that may arise when using transactions in ordinary work (this article mainly @Transactionaldiscusses the use of transaction management) and Corresponding solution

  1. Transaction failure
  2. Issues related to transaction rollback
  3. Problems when read-write separation is used in combination with transactions

Transaction failure

We generally need to troubleshoot issues from two aspects

Database level

At the database level, does the storage engine used by the database support transactions? By default, the MySQL database uses the Innodb storage engine (after version 5.5), which supports transactions, but if your table specifically modifies the storage engine, for example, you modify the storage engine used by the table through the following statement MyISAM: and MyISAMit is not transactional

alter table table_name engine=myisam;

This will cause the "transaction failure" problem

Solution : Modify the storage engine as Innodb.

Business code level

Whether there is a problem with the business-level code, there are many possibilities

  1. We want to use Spring's declarative transaction, so has the bean that needs to execute the transaction been managed by Spring? The embodiment in the code is whether there is a series of annotations on the class @Service, Componentetc.

**Solution:** Hand over the Bean to Spring for management (add @Servicenote)

  1. @TransactionalWhether the annotation is placed in the proper place. In the last article, we did a detailed analysis of the principle of transaction failure in Spring, which also analyzed how Spring internally parses @Transactionalannotations. Let's review the code a bit:

image-20200818152357704

Code is located: AbstractFallbackTransactionAttributeSource#computeTransactionAttributein

In other words, you cannot use @Transactionaltransaction management on a non-public method by default

** Solution: ** Modify the method that requires transaction management public.

  1. Self-calling appeared. What is self-calling? Let's look at an example
@Service
public class DmzService {

	public void saveAB(A a, B b) {
		saveA(a);
		saveB(b);
	}

	@Transactional
	public void saveA(A a) {
		dao.saveA(a);
	}

	@Transactional
	public void saveB(B b){
		dao.saveB(a);
	}
}

The above three methods are in the same class DmzService, where the saveABmethod is called in this class saveAwith the saveBmethod, which is self call. In the example above saveAwith saveBthe transaction will fail on

So why does self-invoking cause transaction failure? We know that the implementation of transactions in Spring is dependent AOP. When the container creates dmzServicethis Bean, it finds that there is an @Transactionalannotated method in this class (modifier is public), then we need to create a proxy object for this class and put it in In the container, the proxy object created is equivalent to the following class

public class DmzServiceProxy {

    private DmzService dmzService;

    public DmzServiceProxy(DmzService dmzService) {
        this.dmzService = dmzService;
    }

    public void saveAB(A a, B b) {
        dmzService.saveAB(a, b);
    }

    public void saveA(A a) {
        try {
            // 开启事务
            startTransaction();
            dmzService.saveA(a);
        } catch (Exception e) {
            // 出现异常回滚事务
            rollbackTransaction();
        }
        // 提交事务
        commitTransaction();
    }

    public void saveB(B b) {
        try {
            // 开启事务
            startTransaction();
            dmzService.saveB(b);
        } catch (Exception e) {
            // 出现异常回滚事务
            rollbackTransaction();
        }
        // 提交事务
        commitTransaction();
    }
}

The above is a piece of pseudo-code, through startTransaction, rollbackTransaction, commitTransactionthe logical agent SIMULATION three classes. Because there are annotations on DmzServicethe saveAfollow saveBmethod in the target class , @Transactionalthese two methods will be intercepted and the transaction management logic will be embedded. At the same time saveAB, there is no method @Transactional, which is equivalent to the proxy class directly calling the method in the target class.

We will find that saveABthe call chain of the entire method when called through the proxy class is as follows:

Transaction failure

In fact, we call saveAnow saveBcalled when a target is a class, this empty, of course, the transaction will fail.

Another example of common transaction failure caused by self-call is as follows:

@Service
public class DmzService {
	@Transactional
	public void save(A a, B b) {
		saveB(b);
	}

	@Transactional(propagation = Propagation.REQUIRES_NEW)
	public void saveB(B b){
		dao.saveB(a);
	}
}

When we call the savemethod, our expected execution flow is like this

Transaction invalidation (self calling requires_new)

In other words, the two transactions do not interfere with each other, and each transaction has its own open, rollback, and commit operations.

But according to the previous analysis, we know that when the saveB method is called, the saveB method in the target class is directly called. There will be no transaction opening or committing or rolling back before and after the saveB method. The actual process is The following

Transaction invalidation (self-calling requires_new) execution flow

Since the saveB method is actually called by dmzService, that is, the target class itself, transaction-related operations are not performed before and after the saveB method. This is also the root cause of the problem caused by self-calling : when self-calling, the method in the target class is called instead of the method in the proxy class

Solution :

  1. Inject yourself and then display the call, for example:

    @Service
    public class DmzService {
    	// 自己注入自己
    	@Autowired
    	DmzService dmzService;
    
    	@Transactional
    	public void save(A a, B b) {
    		dmzService.saveB(b);
    	}
    
    	@Transactional(propagation = Propagation.REQUIRES_NEW)
    	public void saveB(B b){
    		dao.saveB(a);
    	}
    }
    
    

    This scheme does not look very elegant

  2. Use AopContextas follows:

    @Service
    public class DmzService {
    
    	@Transactional
    	public void save(A a, B b) {
    		((DmzService) AopContext.currentProxy()).saveB(b);
    	}
    
    	@Transactional(propagation = Propagation.REQUIRES_NEW)
    	public void saveB(B b){
    		dao.saveB(a);
    	}
    }
    
    

    Use the above solution to note that you need to add a configuration to the configuration class

    // exposeProxy=true代表将代理类放入到线程上下文中,默认是false
    @EnableAspectJAutoProxy(exposeProxy = true)
    
    

    Personally prefer the second way

Here we make a summary

to sum up

A picture is worth a thousand words

Reasons for transaction failure

Issues related to transaction rollback

Issues related to rollback can be summarized in two sentences

  1. The transaction was committed when I wanted to roll back
  2. When you want to submit, it is marked as rollback only (rollback only)

Let's look at the first situation: the transaction is committed when you want to roll back . This situation is often caused by the programmer's rollbackForinadequate understanding of the properties of transactions in Spring .

Spring unchecked by default throws uncheckedan exception (inherited from RuntimeExceptionexception) or Erroronly roll back the transaction; other exception does not trigger roll back the transaction, SQL has been executed submitted out. If you throw other types of exceptions in the transaction, but expect Spring to roll back the transaction, you need to specify the rollbackForproperties.

The corresponding code was actually analyzed in the previous article, as follows:

image-20200818195112983

The above code is located in: TransactionAspectSupport#completeTransactionAfterThrowingmethod

By default, only appear RuntimeExceptionor Errorroll back

public boolean rollbackOn(Throwable ex) {
    return (ex instanceof RuntimeException || ex instanceof Error);
}

So, if you want to roll back RuntimeExceptioneven Errorwhen there is a non- or , please specify the exception during rollback, for example:

@Transactional(rollbackFor = Exception.class)

The second case: when you want to submit it is marked as rollback only (rollback only) .

The corresponding exception information is as follows:

Transaction rolled back because it has been marked as rollback-only

Let's look at an example first

@Service
public class DmzService {

	@Autowired
	IndexService indexService;

	@Transactional
	public void testRollbackOnly() {
		try {
			indexService.a();
		} catch (ClassNotFoundException e) {
			System.out.println("catch");
		}
	}
}

@Service
public class IndexService {
	@Transactional(rollbackFor = Exception.class)
	public void a() throws ClassNotFoundException{
		// ......
		throw new ClassNotFoundException();
	}
}

In the above example, DmzServicethe testRollbackOnlymethod and IndexServicethe amethod both open the transaction, and the propagation level of the transaction is required, so when we testRollbackOnlycall IndexServicethe amethod in the method, these two methods should be a common transaction. According to this kind of thinking, although IndexServicethe amethod throws an exception, but we are testRollbackOnlycatching the exception, then the transaction should be submitted normally. Why is the exception thrown?

If you have read my previous source code analysis article, you should know that there is such a piece of code when dealing with rollback

rollBackOnly setting

The following judgment was made at the time of submission ( I deleted some unimportant codes for this method )

commit_rollbackOnly

It can be seen that when it is submitted that the transaction has been marked as rollbackOnly, it will enter the rollback process, and the unexpected passed in is true. When processing the rollback, there is the following code

Throw an exception

Finally this exception was thrown here.

The above codes are located AbstractPlatformTransactionManagerin

To sum up, the main reason is that the entire large transaction is marked as rollbackOnly when the internal transaction is rolled back , so even if we catch the thrown exception in the external transaction, the entire transaction still cannot be submitted normally, and if you want it to be normal Submit, Spring will also throw an exception.

Solution :

The solution depends on the business, you have to be clear about what the result you want is

  1. Abnormalities occur in internal affairs, and after external affairs catch exceptions, internal affairs will automatically roll back without affecting external affairs

The propagation level of internal affairs can be set to nested/requires_new. In our example, the following modifications are made:

// @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
@Transactional(rollbackFor = Exception.class,propagation = Propagation.NESTED)
public void a() throws ClassNotFoundException{
// ......
throw new ClassNotFoundException();
}

Although both can get the above results, there are still differences between them. When the propagation level requires_new, the two transactions are totally unrelated, each has its own transaction management mechanisms (open transaction, the transaction is closed, roll back the transaction). But the spread level nestedjust set a save point when you call a method, in fact, there is only one transaction, when a rollback method actually roll back to the save point, and when the external transaction commits, internal The transaction will be committed, and if the external transaction is rolled back, the internal transaction will be rolled back.

  1. When an internal transaction is abnormal, after the external transaction catches the exception, both internal and external transactions are rolled back, but the method does not throw an exception
@Transactional
public void testRollbackOnly() {
try {
   indexService.a();
} catch (ClassNotFoundException e) {
   // 加上这句代码
   TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
}
}

The status of the transaction is displayed through the settings RollbackOnly. So when the transaction is committed, the following code will be entered

Show rollback

The biggest difference is that false is passed in the second parameter when processing the rollback, which means that the rollback is expected, so no exception will be thrown after the rollback is processed.

Problems when read-write separation is used in combination with transactions

There are generally two ways to achieve read-write separation

  1. Configure multiple data sources
  2. Rely on middleware, such asMyCat

If multiple data sources are configured to achieve read-write separation, you need to pay attention to: if a read-write transaction is enabled, then a write node must be used , if it is a read-only transaction, then a read node can be used

If you rely on MyCatmiddleware, you need to pay attention: as long as the transaction is enabled, the SQL in the transaction will use the write node (depending on the implementation of the specific middleware, it may also be allowed to use the read node, the specific strategy needs to be confirmed with the DB team)

Based on the above conclusions, we should be more cautious when using transactions, and try not to open them when there is no need to open them.

Generally, we will configure certain agreed method name prefixes in the configuration file to enable different transactions (or not to enable them), but now with the popularity of annotation transactions, many developers (or architects) add to the service class when building a framework On the @Transactional annotation, the entire class is opened for transactions, which seriously affects the efficiency of database execution. More importantly, developers don’t pay attention to it or don’t know how to add @Transactional(propagation=Propagation .NOT_SUPPORTED) will result in that all the query methods do not actually go to the slave library, resulting in excessive pressure on the master library.

Secondly, if there is no optimization for read-only transactions (optimization means routing read-only transactions to read nodes), then @Transactionalthe readOnlyattributes in the annotations should be used with caution. readOnlyThe original purpose of our use is to mark the transaction as read-only, so that when the MySQL server detects that it is a read-only transaction, it can optimize and allocate less resources (for example: read-only transactions do not need to be rolled back, so there is no need Allocate undo log segment). However, when read-write separation is configured, it may cause all SQL in the read-only transaction to be routed to the main database, and read-write separation loses its meaning.

Guess you like

Origin blog.csdn.net/weixin_47067712/article/details/108125759