Transaction failure

Transaction failures are generally considered in two ways

Database level

At the database level, does the storage engine used by the database support transactions? By default MySQL database uses Innodb storage engine (after version 5.5), which supports transactions, but if your table specifically changes the storage engine, for example, you change the storage engine to MyISAM by using the following statement, which does not support transactions

alter table table_name engine=myisam;
Copy the code

This leads to the problem of “transaction invalidation”

Solution: Change the storage engine to Innodb.

Business code level

There are many possibilities as to whether there is a problem with the code at the business level

  1. We are going to use Spring’s declarative transactions. Are the beans that need to perform transactions already managed by Spring? This is reflected in the code if there are @Service, Component, etc. annotations on the class

“Solution” : Commit beans to Spring management (add @service annotation)

  1. Whether the @Transactional annotation is put in place. In the previous article we looked at transaction failures in Spring in more detail. We also looked at how the @Transactional annotation is resolved internally. Let’s review the code:

Annotations parsing

The code is located in: AbstractFallbackTransactionAttributeSource# computeTransactionAttribute

That is, by default you cannot use @Transactional for transaction management of a non-public method

“Solution” : Change the method requiring transaction management to public.

  1. Self-invocation occurs. What is self-invocation? 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 all in the same class DmzService, where saveAB method calls saveA and saveB method of this class, which is called by itself. In the above example, transactions on saveA and saveB are invalidated

So why does self-invocation invalidate a transaction? The Transactional implementation in Spring relies on AOP. When a container creates a Bean called dmzService and finds a @Transactional method (public) in that class, it needs to create a proxy object for that class. 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) {// An Exception occurred to rollback the transaction rollbackTransaction(); } // commitTransaction(); } public void saveB(B B) {try {// startTransaction(); dmzService.saveB(b); } catch (Exception e) {// An Exception occurred to rollback the transaction rollbackTransaction(); } // commitTransaction(); }}Copy the code

Above is pseudocode that simulates the logic implemented by the proxy class through the startTransaction, rollbackTransaction, and commitTransaction methods. SaveA and saveB methods have the @Transactional annotation in DmzService, so they are intercepted and embedded with transaction management logic. SaveAB methods do not have @Transactional annotations. The proxy class directly calls a method in the target class.

We’ll see that when saveAB is called through the proxy class the entire method invocation chain looks like this:

In fact, when we call saveA and saveB, we call the methods in the target class, in which case, of course, the transaction will fail.

Another example of a common self-invoked transaction failure 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);
 }
Copy the code

When we call the save method, we expect the execution flow to look like this

This means that two transactions do not interfere with each other, and each transaction has its own open, rollback, and commit operations.

However, according to the previous analysis, in fact, when saveB method is called, saveB method in the target class is directly called. Before and after saveB method, there will be no transaction opening, submission, rollback and other operations. The actual process is as follows

Since the saveB method is actually called by dmzService (the target class) itself, no transaction operations are performed before or after the saveB method. This is also the root cause of the problem with self-invocation: “Self-invocation calls methods in the target class rather than methods in the proxy class.”

“Solution” :

  1. Inject itself and then display the call, for example: @service

    Public class DmzService {// Autowired 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 doesn’t look very elegant

2. Use AopContext as 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);
 }
}
Copy the code

The important thing to note with this solution is that you need to add a new configuration to the configuration class

// exposeProxy=true puts the proxy class into the thread context. The default is false

@enableAspectJAutoProxy (exposeProxy = true) My personal preference is the second method

So let’s do a little summary here

conclusion

A picture is worth a thousand words

Cause of transaction failure

Transaction rollback related issues

Rollback related issues can be summed up in two sentences

  1. Transaction committed when trying to roll back
  2. Rollback only is marked when you want to commit.

Let’s look at the first case: “The transaction is committed when you want to roll back.” This is often the result of programmers not knowing enough about the rollbackFor property of transactions in Spring.

By default, Spring throws unchecked exceptions (exceptions inherited from RuntimeException) or errors to roll back a transaction; Other exceptions do not trigger a rollback transaction, and SQL that has already been executed is committed. 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.

In fact, we also analyzed the corresponding code in the previous article, as follows:

Roll back the code

The above code is located in: TransactionAspectSupport# completeTransactionAfterThrowing method

By default, only runtimeExceptions or errors are rolled back

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

So, if you now roll back non-runtimeException or Error, specify the exception for the rollback, for example:

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

The second case is “rollback only” when you want to commit.

The corresponding exception information is as follows:

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

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(); }}Copy the code

In this example, both DmzService testRollbackOnly and IndexService A have transactions enabled, and the propagation level of the transaction is required. So when we call IndexService method A in testRollbackOnly the two methods should share a transaction. According to this train of thought, although IndexService a method throws exceptions, but we in the abnormal testRollbackOnly will capture, then the transaction should be submitted by the normal, why will throw an exception?

If you’ve read my previous source code analysis article, you’ll know that there’s a code that handles rollback

RollBackOnly set

I also made the following judgment when committing (I cut out some unimportant code for this method)

commit_rollbackOnly

As you can see, the transaction is entered into rollback processing when it is found to have been marked rollbackOnly, and UNEXPECTED passes in true. Here’s another code to handle a rollback

An exception is thrown

And I ended up throwing this exception here.

The above code is located in AbstractPlatformTransactionManager

To sum up, “the main reason is because the internal transaction is rolled back with a rollbackOnly flag for the entire large transaction”, so even if we catch the exception thrown in the external transaction, the entire transaction will not commit properly, and Spring will throw an exception if you want to commit normally.

“Solution” :

The solution depends on the business. What do you want the result to be

  1. If an exception occurs in an internal transaction and an external transaction catch exception occurs, the internal transaction automatically rolls back, without affecting the external transaction

The propagation level of an internal transaction can be set to NESTED/REQUIRES_new. In our example, the following changes 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 of these can get the above results, there are differences between them. When propagation level is REQUIRES_new, the two transactions are completely unrelated and each has its own transaction management mechanism (open transaction, close transaction, roll back transaction). However, when the propagation level is nested, only a savepoint is set when method A is called. When method A is rolled back, the savepoint is actually rolled back, and the internal transaction is committed when the external transaction is committed. If the external transaction is rolled back, the internal transaction is also rolled back.

  1. When an exception occurs in an internal transaction, both transactions are rolled back after an external transaction catch exception, but the method does not throw an exception

@Transactional

public void testRollbackOnly() {

try {

indexService.a();

} catch (ClassNotFoundException e) {

// Add this code

TransactionInterceptor.currentTransactionStatus().setRollbackOnly(); }}

Set the transaction status to RollbackOnly by the display. This will enter the following code when the transaction is committed

According to the rollback

The biggest difference is that the second argument is passed false when the rollback is handled, which means that the rollback is expected, so no exception is thrown after the rollback is handled.

Problems with read-write separation when used in conjunction with transactions

Read/write separation is generally implemented in two ways

  1. Configuring multiple Data Sources
  2. Rely on middleware, such as MyCat

If multiple data sources are configured to achieve read/write separation, note that: “If a read/write transaction is enabled, then the write node must be used”, “if a read-only transaction, then the read node can be used.”

If it depends on MyCat or other middleware, it should be noted that “as long as the transaction is enabled, the SQL in the transaction will use write nodes (depending on the implementation of specific middleware, read nodes may be allowed, the specific policy 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 start them when there is no need to.

The Transactional Transactional annotation is often used to prefix the Transactional method name in a configuration file to open different transactions (or not). However, with the popularity of annotated transactions, many developers (or architects) framework their service classes with the @Transactional annotation, resulting in the Transactional annotation of the entire class. Transactional (Propagation = Propagandis.not_supported) means that all query methods don’t actually make it through the library. The main library is under too much pressure.

Second, the readOnly attribute in the @Transactional annotation should be used with caution if read-only transactions are not optimized (which means routing read-only transactions to read nodes). The original purpose of readOnly 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 to allocate undo log segments). However, when read/write separation is configured, it is possible to cause all SQL in read-only transactions to be routed to the master library, and read/write separation becomes meaningless.