Location>code7788 >text

After 6 years of work, the @Transactional annotation is a mess!

Popularity:509 ℃/2024-08-30 11:28:29

Taking on a new project is hard to say, not to mention the fact that a@Transactional Annotations are used all over the place, with a wide variety of uses, and a good portion of them also fail without being able to be rolled back.

Consciously add in transaction-related methods that involve@Transactionalannotation, is a good habit. However, many students just add this annotation subconsciously, and once the function is running normally, very few of them will verify whether the transaction can be rolled back correctly in the case of an exception. Although the @Transactional annotation is easy to use, it can always fail in some unexpected situations, which is hard to prevent!

I have grouped these transactional issues into three categories:unduenot in forcenon-rollbackThe next step is to use some demos to demonstrate the respective scenarios.

undue

1. Operations not requiring transactions

The @Transactional annotation is used on business methods that are not transactional, e.g., methods that are query-only or HTTP request-only. Although this does not have a significant impact, it is not sufficiently strict from a coding perspective, and it is recommended that it be removed.

@Transactional
public String testQuery() {
    (1L);
    return "testB";
}

2. Excessive scope of services

Some people put the @Transactional annotation on classes or abstract classes to save time, which leads to the problem ofAll methods within a class or in an implementation of an abstract class are transactionally managed.. Adds unnecessary performance overhead or complexity, it is recommended to use it on an as-needed basis and only add @Transactional to methods that have transaction logic.

@Transactional
public abstract class BaseService {
}

@Slf4j
@Service
public class TestMergeService extends BaseService{

    private final TestAService testAService;

    public String testMerge() {

        ();

        return "ok";
    }
}

If you add the @Transactional annotation to a method in a class, it overrides the transaction configuration at the class level. For example, if a read-only transaction is configured at the class level, the @Transactional annotation at the method level overrides that configuration to enable read-write transactions.

@Transactional(readOnly = true)
public class TestMergeService {

    private final TestBService testBService;

    private final TestAService testAService;

    @Transactional
    public String testMerge() {

        ();

        ();
        return "ok";
    }
}    

not in force

3. Issues of methodological competence

Don't put the @Transactional annotation on private level methods!

We know that the @Transactional annotation relies on the Spring AOP cutout to enhance transactional behavior. This AOP is implemented through proxies, and private methods cannot be proxied, so AOP enhancements to private methods are ineffective, and @Transactional will not take effect.

@Transactional
private String testMerge() {

    ();

    ();

    return "ok";
}

So if I call the private method transaction inside the testMerge() method will it work?

Answer: Services will be effective

@Transactional
public String testMerge() throws Exception {

    ccc();
    
    return "ok";
}

private void ccc() {
    ();

    ();
}

4. the method is qualified by final, static

For similar reasons as above, being usedfinalstatic Adding @Transactional to a modified method won't work either.

  • static Static methods belong to the class itself and are not instances, so the proxy mechanism cannot proxy or intercept static methods.
  • Final-modified methods cannot be overridden by subclasses, transaction-related logic cannot be inserted into final methods, and proxies cannot intercept or augment final methods.

These are basic java concepts now, use them with care.

@Transactional
public static void b() {
}

@Transactional
public final void b() {
}

5. Problems with internal method calls in the same class

Attention.It happens all the time!

Calls between methods within the same class are the hardest hit areas where the @Transactional annotation fails, and online you'll always see that when a method internally calls another method of the same class, theSuch calls do not go through the proxyTherefore, transaction management would not be effective. However, this is a rather one-sided statement and depends on the circumstances.

For example, the testMerge() method opens a transaction and calls similar non-transactional methods a() and b(), at which point b() throws an exception, and according to the propagation of the transaction the a() and b() transactions take effect.

@Transactional
public String testMerge() {

    a();

    b();

    return "ok";
}

public void a() {
    (());
}

public void b() {
    (testBService.buildEntity2());
    throw new RuntimeException("b error");
}

If the testMerge() method does not have transactions turned on and calls the non-transactional method a() and the transactional method b() in the same class, when b() throws an exception, the transactions for both a() and b() do not take effect. This is because such calls are made directly through thethis The object is carried out, not proxied, so transaction management cannot take effect. This often goes wrong!

public String testMerge() {

    a();

    b();

    return "ok";
}

public void a() {
    (());
}

@Transactional
public void b() {
    (testBService.buildEntity2());
    throw new RuntimeException("b error");
}

5.1 Separate Service Classes

It's easy to make the b() method transactional. The easiest way to do this is to strip it down and put it in a separate Service class for injection and use, and leave it to spring to manage. However, this way will create a lot of classes.

@Slf4j
@Service
public class TestBService {

      @Transactional
      public void b() {
          (testBService.buildEntity2());
          throw new RuntimeException("b error");
      }
}

5.2 Self-injection method

Or by injecting themselves into their own way to solve the problem, although the solution to the problem, the logic looks strange, it breaks the principle of dependency injection, although spring supports us to use this way, but still have to pay attention to the problem of circular dependencies.

@Slf4j
@Service
public class TestMergeService {
      @Autowired
      private TestMergeService testMergeService;

      public String testMerge() {

          a();

          ();

          return "ok";
      }

      public void a() {
          (());
      }

      @Transactional
      public void b() {
          (testBService.buildEntity2());
          throw new RuntimeException("b error");
      }
}

5.3 Getting Proxy Objects Manually

The b() method is not proxied, so it's fine to get the proxy object and call the b() method manually. We can get the proxy object manually by calling the b() method with the() method returns the current instance of the proxy object, so that calls to the proxy's methods go through the AOP cutout and the @Transactional annotation takes effect.

@Slf4j
@Service
public class TestMergeService {

      public String testMerge() {

          a();

         ((TestMergeService) ()).b();

          return "ok";
      }

      public void a() {
          (());
      }

      @Transactional
      public void b() {
          (testBService.buildEntity2());
          throw new RuntimeException("b error");
      }
}

6. Bean not managed by spring

Above, we know that the @Transactional annotation manages transactions through AOP, which relies on the proxy mechanism. Therefore, theThe bean must be managed as an instance by Spring! Be sure to add to classes such as@Controller@Service maybe@Componentannotation to be managed by Spring, which is easy to overlook.

@Service
public class TestBService {

    @Transactional
    public String testB() {
        (entity2);
        return "testB";
    }
}

7. Asynchronous thread calls

If we use an asynchronous thread in the testMerge() method to perform a transaction operation, it is also usually not successfully rolled back, to give a concrete example.

The testMerge() method calls testA() in a transaction, and the transaction is turned on in the testA() method. Then, in the testMerge() method, we call testB() from a new thread, which also opens the transaction and throws an exception in testB().

What does the rollback look like at this point?

@Transactional
public String testMerge() {

    ();

    new Thread(() -> {
        try {
            ();
        } catch (Exception e) {
//                ();
            throw new RuntimeException();
        }
    }).start();

    return "ok";
}

@Transactional
public String testB() {
    DeepzeroStandardBak2 entity2 = buildEntity2();

    (entity2);

    throw new RuntimeException("test2");
}

@Transactional
public String testA() {
    DeepzeroStandardBak entity = buildEntity();
    (entity);
    return "ok";
}

The answer is that transactions in both testA() and testB() are not rolled back.

testA() cannot be rolled back because the exception thrown by testB() in the new thread is not caught; the testB() method cannot be rolled back because the transaction manager is only valid for transactions in the current thread, so transactions executed in the new thread are not rolled back.

Because Spring's transaction manager does not propagate transactions across threads in a multithreaded environment, the state of the transaction (e.g., whether the transaction is open or not) is stored locally in the threads.ThreadLocal to store and manage transaction context information. This means that each thread has a separate transaction context and transaction information is not shared between threads.

8. Engines that do not support transactions

Database engines that do not support transactions are not in thisReview Within the scope, it's good to just be aware. The relational databases we typically use, such as MySQL, by default use a transaction-enabledInnoDB engine, rather than the transactionalMyISAM The engine is less used.

Previously, enabling MyISAM engines was turned on to improve query efficiency. However, nowadays, non-relational databases such asRedisMongoDB cap (a poem)Elasticsearch and other middleware provides a more cost-effective solution.

non-rollback

9. Misuse of communication attributes

@TransactionalThe annotation has a key parameterpropagation, which controls the propagation behavior of transactions, and sometimes misconfiguration of transaction propagation parameters can lead to non-rollback of transactions.

propagation supports seven transaction propagation features:

  • REQUIREDDefault propagation behavior, if there is no current transaction, a new transaction is created; if a transaction exists, it is added to the current transaction.
  • MANDATORY: supports the current transaction and throws an exception if it does not exist
  • NEVER: non-transactional execution, throwing an exception if a transaction exists
  • REQUIRES_NEW: Whether or not a transaction currently exists, a new transaction is created and the original transaction is hung.
  • NESTED: Nested transactions, where the called method runs in a nested transaction that depends on the current transaction.
  • SUPPORTS: If a transaction currently exists, it is added; if not, it is executed in a non-transactional manner.
  • NOT_SUPPORTED: Execute in a non-transactional manner and hang the transaction if it currently exists.

To make it more impressive, I'll use a case study to simulate a scenario in which each feature is used.

REQUIRED

REQUIRED is the default transaction propagation behavior. If the testMerge() method has a transaction enabled, then its internally called testA() and testB() methods will also join the transaction. If testMerge() does not enable transactions, and the @Transactional annotation is used on the testA() and testB() methods, each of those methods will create a new transaction, controlling only their own rollback.

@Component
@RequiredArgsConstructor
@Slf4j
@Service
public class TestMergeService {

    private final TestBService testBService;

    private final TestAService testAService;

    @Transactional
    public String testMerge() {

        ();

        ();

        return "ok";
    }
}

@Transactional
public String testA() {
    ("testA");
    DeepzeroStandardBak entity = buildEntity();
    (entity);
    return "ok";
}

@Transactional
public String testB() {
    ("testB");
    DeepzeroStandardBak2 entity2 = buildEntity2();
    (entity2);
    throw new RuntimeException("testB");
}

MANDATORY

The MANDATORY propagation feature simply means that it can only be called by higher level methods that have transactions turned on. For example, if the testMerge() method calls the testB() method without a transaction turned on, then an exception will be thrown; if the testMerge() method calls the testB() method with a transaction turned on, then it will be added to the current transaction.

@Component
@RequiredArgsConstructor
@Slf4j
@Service
public class TestMergeService {

    private final TestBService testBService;

    private final TestAService testAService;

    public String testMerge() {

        ();

        ();

        return "ok";
    }
}

@Transactional
public String testA() {
    ("testA");
    DeepzeroStandardBak entity = buildEntity();
    (entity);
    return "ok";
}

@Transactional(propagation = )
public String testB() {
    ("testB");
    DeepzeroStandardBak2 entity2 = buildEntity2();
    (entity2);
    throw new RuntimeException("testB");
}

Exception information thrown

: No existing transaction found for transaction marked with propagation 'mandatory'

NEVER

The NEVER propagation feature forces your methods to run only in a non-transactional way, and throws an exception if there is a transactional operation on the method, so I really don't see any scenarios for using it.

@Transactional(propagation = )
public String testB() {
    ("testB");
    DeepzeroStandardBak2 entity2 = buildEntity2();
    (entity2);
//        throw new RuntimeException("testB");
    return "ok";
}

Exception information thrown

: Existing transaction found for transaction marked with propagation 'never'

REQUIRES_NEW

When we use the Propagation.REQUIRES_NEW propagation feature, calling this method creates a new transaction regardless of the state of the current transaction.

For example, the testMerge() method starts a transaction, and when the testB() method is called, it suspends the testMerge() transaction and starts a new one. If an exception occurs within the testB() method, the new transaction is rolled back, but the original pending transaction is not affected. This means that the pending transaction is not affected by the rollback of the new transaction, nor is it affected by the failure of the new transaction.


@Transactional
public String testMerge() {

    ();

    ();

    return "ok";
}

@Transactional
public String testA() {
    ("testA");
    DeepzeroStandardBak entity = buildEntity();
    (entity);
    return "ok";
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public String testB() {
    ("testB");
    DeepzeroStandardBak2 entity2 = buildEntity2();
    (entity2);
    throw new RuntimeException("testB");
}

NESTED

method's propagation behavior is set to NESTED, its internal method opens a new nested transaction (sub-transaction). In the absence of an external transactionNESTED together withREQUIRED The effect is the same; in the presence of an external transaction, it creates a nested transaction (child transaction) once the external transaction is rolled back.

This means that when an external transaction is rolled back, the child transaction is rolled back along with it; however, the rollback of the child transaction will not affect the external transaction or other sibling transactions.

@Component
@RequiredArgsConstructor
@Slf4j
@Service
public class TestMergeService {

    private final TestBService testBService;

    private final TestAService testAService;

    @Transactional
    public String testMerge() {

        ();

        ();

        throw new RuntimeException("testMerge");
        return "ok";
    }
}

@Transactional
public String testA() {
    ("testA");
    DeepzeroStandardBak entity = buildEntity();
    (entity);
    return "ok";
}

@Transactional(propagation = )
public String testB() {
    ("testB");
    DeepzeroStandardBak2 entity2 = buildEntity2();
    (entity2);
    throw new RuntimeException("testB");
}

NOT_SUPPORTED

NOT_SUPPORTED The transaction propagation characteristic indicates that the method must be run in a non-transactional manner. When the method testMerge() opens a transaction and calls the transactional methods testA() and testB(), if the transaction propagation characteristics of testA() and testB() are NOT_SUPPORTED, then testB() runs non-transactional and hangs the current transaction.

The default propagation of features in the case of testB() will cause testA() to be rolled back if an exception transaction is added, whereas pending means that testB() will not affect the rollback of the other testA() methods in testMerge() if an exception is thrown within it.

@Component
@RequiredArgsConstructor
@Slf4j
@Service
public class TestMergeService {

    private final TestBService testBService;

    private final TestAService testAService;

    @Transactional
    public String testMerge() {

        ();

        ();

        return "ok";
    }
}

@Transactional
public String testA() {
    ("testA");
    DeepzeroStandardBak entity = buildEntity();
    (entity);
    return "ok";
}

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public String testB() {
    ("testB");
    DeepzeroStandardBak2 entity2 = buildEntity2();
    (entity2);
    throw new RuntimeException("testB");
}

SUPPORTS

If the transaction propagation characteristic of the current method isSUPPORTS, then the method's transaction will be valid only if the higher-level method calling the method has transactions turned on. If the higher-level method does not have transactions turned on, then the method's transaction feature will be invalid.

For example, if the entry method testMerge() does not have transactions turned on, and the methods testA() and testB() called by testMerge() have the transaction propagation characteristic of SUPPORTS, then since testMerge() does not have transactions, testA() and testB() will be executed in a non-transactional manner. Even if you add the@Transactional annotation, and no exceptions will be rolled back.

@Component
@RequiredArgsConstructor
@Slf4j
@Service
public class TestMergeService {

    private final TestBService testBService;

    private final TestAService testAService;

    public String testMerge() {

        ();

        ();

        return "ok";
    }
}

@Transactional(propagation = )
public String testA() {
    ("testA");
    DeepzeroStandardBak entity = buildEntity();
    (entity);
    return "ok";
}

@Transactional(propagation = )
public String testB() {
    ("testB");
    DeepzeroStandardBak2 entity2 = buildEntity2();
    (entity2);
    throw new RuntimeException("testB");
}

10. Self-swallowed anomalies

Throughout the review process I found that most of the scenarios that resulted in transactions not rolling back were the result of developers manually trying... ...catching an exception in business code and then not throwing it. .catch caught the exception, and then did not throw the exception ....

For example, the testMerge() method opens a transaction and calls the non-transactional methods testA() and testB(), and catches an exception in testMerge(). If an exception occurs in testB() and is thrown, but testMerge() catches the exception and doesn't continue to throw it, the Spring transaction won't be able to catch the exception, and you won't be able to roll back.

@RequiredArgsConstructor
@Slf4j
@Service
public class TestMergeService {

    private final TestBService testBService;

    private final TestAService testAService;
    @Transactional
    public String testMerge() {

        try {
            ();

            ();

        } catch (Exception e) {
            ("testMerge error:{}", e);
        }
        return "ok";
    }
}

@Service
public class TestAService {

    public String testA() {
        (entity);
        return "ok";
    }
}

@Service
public class TestBService {

    public String testB() {
        (entity2);
        
        throw new RuntimeException("test2");
    }
}

In order to ensure that a Spring transaction can be rolled back properly, we need to proactively rethrow the RuntimeException or Error type of exception that it can handle in the catch block.

@Transactional
public String testMerge() {

    try {
        ();

        ();

    } catch (Exception e) {
        ("testMerge error:{}", e);
        throw new RuntimeException(e);
    }
    return "ok";
}

Just because an exception is caught doesn't mean it won't be rolled back.It depends on the circumstances.

For example, when the @Transactional annotation is also added to the testB() method, if an exception occurs in that method, the transaction will catch the exception. Due to the nature of transaction propagation, the transaction of testB() is merged into the transaction of the higher-level method. Therefore, even if an exception is caught in testMerge() and not thrown, the transaction can still be successfully rolled back.

@Transactional
public String testB() {

    DeepzeroStandardBak2 entity2 = buildEntity2();

    (entity2);

    throw new RuntimeException("test2");
    // return "ok";
}

However, this comes with the caveat that the @Transactional annotation must be added to the testMerge() method to enable transactions. If the testMerge() method is not transaction-enabled, testB() can only be partially rolled back, but testA() will not be rolled back, regardless of whether it uses a try block internally.

11. Exceptions not caught by the transaction

Spring transactions are rolled back by default RuntimeException and its subclasses, andError Type of exception.

If another type of exception is thrown, such as achecked exceptions(checked exceptions), i.e. exceptions that inherit from Exception but not from RuntimeException, such asSQLExceptionDuplicateKeyExceptionThe transaction will not be rolled back.

So, when we actively throw an exception, we make sure that the exception is of a type that the transaction can catch.

@Transactional
public String testMerge() throws Exception {
    try {
        ();

        ();
    } catch (Exception e) {
        ("testMerge error:{}", e);
//            throw new RuntimeException(e);
        throw new Exception(e);
    }
    return "ok";
}

If you have to throw an exception that by default will not result in a transaction rollback, be sure to add a new exception to the@Transactional annotatedrollbackFor parameter explicitly specifies the exception so that it can be rolled back.

@Transactional(rollbackFor = )
public String testMerge() throws Exception {
    try {
        ();

        ();
    } catch (Exception e) {
        ("testMerge error:{}", e);
//            throw new RuntimeException(e);
        throw new Exception(e);
    }
    return "ok";
}

Ask your classmates around you which exceptions are runtime exceptions and which are checked exceptions, and nine times out of ten they probably won't be able to give you an accurate answer!

To minimize the risk of bugs, I recommend using the @Transactional annotation with a rollbackFor parameter ofException maybeThrowable, which extends the scope of the transaction rollback.

12. The issue of customizing the scope of exceptions

It is common practice to customize exception types for different businesses. The @Transactional annotation's rollbackFor parameter supports custom exceptions, but we are often used to inheriting these custom exceptions from RuntimeException.

This then presents the same problem as above, where the transaction is under-scoped and many exception types still fail to trigger a transaction rollback.

@Transactional(rollbackFor = )
public String testMerge() throws Exception {
    try {
        ();

        ();
    } catch (Exception e) {
        ("testMerge error:{}", e);
//            throw new RuntimeException(e);
        throw new Exception(e);
    }
    return "ok";
}

To solve this problem, you can actively throw our custom exception in catch.

@Transactional(rollbackFor = )
public String testMerge() throws Exception {
    try {
        ();

        ();
    } catch (Exception e) {
        ("testMerge error:{}", e);
        throw new CustomException(e);
    }
    return "ok";
}

13. Nested transactions

Another scenario is the nested transaction problem. For example, if we call the transaction method testA() and the transaction method testB() in the testMerge() method, we don't want testB() to throw an exception and roll back the whole testMerge(); we need to have a separate try catch to handle the exception of testB(), so as not to let it throw upwards. up.

@RequiredArgsConstructor
@Slf4j
@Service
public class TestMergeService {

    private final TestBService testBService;

    private final TestAService testAService;
    @Transactional
    public String testMerge() {
    
        ();

        try {
            ();
        } catch (Exception e) {
            ("testMerge error:{}", e);
        }
        return "ok";
    }
}

@Service
public class TestAService {

    @Transactional
    public String testA() {
        (entity);
        return "ok";
    }
}

@Service
public class TestBService {

    @Transactional
    public String testB() {
        (entity2);
        
        throw new RuntimeException("test2");
    }
}

summarize

The above notes on the use of the @Transactional annotation were compiled by me after code review and gathering ideas from the web. I've written a similar article before, but it wasn't comprehensive enough. This time, I've added more details. The development work is only a small part of the overall workload, and more time is actually spent on self-testing and validation. I hope these cases will be useful to you and you will step into fewer potholes.