This is the 16th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021

Transactional management in Spring is easy. Simply annotate a method called @Transactional and Spring automatically starts, commits, and rolls back transactions for you. Many people even mentally equate Spring transactions with @Transactional, annotating methods with @Transactional whenever they have database related operations.

To tell you the truth, THAT was always the case with me until using @Transactional led to a production accident that resulted in a D for my performance that month…

@TransactionalThe resulting production accident

In nineteen nineteen, I made an internal reimbursement project in the company, with the following business logic:

1. Employees who work overtime can take taxi directly through Didi Chuxing Enterprise Edition, and the taxi fees for the next day can be directly synchronized to our reimbursement platform

2. Employees can check their taxi fare on the reimbursement platform and create an reimbursement form for reimbursement. When the reimbursement form is created, an approval flow (unified process platform) will be created for approval by leaders

Here’s the code for creating an expense report:

*/ @transactional (rollbackFor = exception.class) public void save(RequestBillDTO RequestBillDTO){ CreateFlow ("BILL",requestBillDTO); workflowUtil.createFlow("BILL",requestBillDTO); RequestBill = jkMappingutils. convert(requestBillDTO, requestBill.class); requestBillDao.save(requestBill); Requestdetaildao.save (requestBill.getDetail())}Copy the code

The code is very simple and “elegant”. It calls the workflow engine through the HTTP interface to create the approval flow, then saves the expense report. To ensure Transactional operations, add the @Transactional annotation to the whole method (come to think of it, does that really guarantee transactions?). .

The reimbursement project belongs to the internal project of the company, which has no high concurrency, and the system has been running stably.

One afternoon at the end of the year (it had been snowing heavily a few days earlier and there were a lot of people taking taxis), the company sent an email informing them that the annual reimbursement window was closing and the unreimbursed expenses needed to be reimbursed as soon as possible, just as the workflow engine was undergoing security reinforcement.

After receiving the email, the number of people claiming reimbursement gradually increased, and reached the peak near the end of work. At this time, the reimbursement system began to break down: the database monitoring platform has been receiving alarm messages, and the database connection is insufficient, resulting in a large number of deadlocks. The log shows that a large number of timeouts occurred when calling the process engine interface. At the same time has been prompt CannotGetJdbcConnectionException, database connection pool connection.

After the failure, we tried to kill the deadlocked process and forcibly restart it, only to have the failure occur again in less than 10 minutes and receive a lot of phone complaints. Finally, there was no choice but to send out a maintenance email to all staff and send out a failure report, and then, performance got a D, oops… .

Accident cause analysis

Analyzing the logs, it is easy to pinpoint the fault of the save() method for saving expense accounts, and the @Transactional annotation as the culprit.

The @Transactional annotation, as we know, is implemented using AOP and essentially intercepts the target method before and after execution. Join or create a transaction before the target method executes, and commit or roll back the transaction after the execution method executes, depending on the situation.

When Spring encounters this annotation, it automatically takes the Connection from the database connection pool, starts the transaction and binds it to ThreadLocal. The entire method wrapped in the @Transactional annotation uses the same Connection. If we have time-consuming operations, such as third-party interface invocation, complex business logic, and mass data processing, we will occupy the connection for a long time, and the database connection will always be occupied and not released. If there are too many such operations, the database connection pool will be exhausted.

The database connection pool overflow caused by RPC operation in a transaction is a typical long transaction problem. Similar operations also have a large amount of data query in a transaction, business rule processing, etc..

What are long transactions?

As the name implies, a transaction that runs for a long time and has not been committed for a long time can also be called a large transaction.

What are the problems caused by long transactions?

Common hazards caused by long transactions include:

  1. The database connection pool is full, and the application cannot obtain connection resources.
  2. Easy to cause database deadlock;
  3. The database rollback takes a long time.
  4. In master-slave architecture, the master-slave latency increases.

How to avoid long transactions?

Now that you know the dangers of long transactions, how can you avoid long transactions in your development?

Obviously, the purpose of solving long transaction is to split the transaction method, make the transaction smaller, faster and reduce the granularity of the transaction as far as possible.

Now that transaction granularity is mentioned, let’s review the way Spring manages transactions.

Declarative transaction

First, transaction-managed operations using the @Transactional annotation on methods are called declarative transactions.

The advantage of declarative transactions is that they are easy to use and can automatically start, commit, and roll back transactions. With this approach, the programmer only needs to focus on the business logic.

One of the biggest disadvantages of declarative transactions is that the granularity of the transaction is the whole method and cannot be controlled with refinement.

The opposite of declarative transactions is programmatic transactions.

Based on the underlying API, the developer manually manages the opening, submission, rollback and other operations of transactions in the code. You can use objects of the TransactionTemplate class in spring projects to manually control transactions.

@Autowired private TransactionTemplate transactionTemplate; . public void save(RequestBill requestBill) { transactionTemplate.execute(transactionStatus -> { requestBillDao.save(requestBill); Requestdetaildao.save (requestBill.getDetail())); return Boolean.TRUE; }); }Copy the code

The greatest benefit of using programmatic transactions is the ability to fine-tune the transaction scope.

So the easiest way to avoid long transactions is not to use declarative transactions @Transactional, but to use programmatic transactions to manually control transaction scopes.

Some students will say that @Transactional is so easy to use. Is there a way to use @Transactional without having to make long transactions?

Then you need to split the method, separating the logic that does not require transaction management from the transaction operations:

@Service public class OrderService{ public void createOrder(OrderCreateDTO createDTO){ query(); validate(); saveData(createDTO); } @transactional (rollbackFor = throwable.class) public void saveData(OrderCreateDTO createDTO){ orderDao.insert(createDTO); }}Copy the code

Query () and validate() do not require transactions; we separate them from the transaction method saveData().

Of course, this splitting would hit the classic scenario where transactions don’t work when using the @Transactional annotation, a mistake that many newbies can easily make. Declarative transactions with the @Transactional annotation work through Spring AOP, which requires proxy objects to be generated. Methods called directly in the same class use the original object. Transactions do not take effect. Several other common scenarios where transactions do not take effect are:

  • Transactional applies to methods that are not public ornamented
  • The propagation setting of the @Transactional attribute is incorrect
  • The @Transactional annotation attribute rollbackFor is incorrectly set
  • A method call in the same class causes @Transactional to fail
  • An exception caught by a catch causes @Transactional to fail

The proper split method should use the following two methods:

  1. You can put a method into another class, such as addThe manager layerWith Spring injection, this meets the criteria for calling between objects.
@Service public class OrderService{ @Autowired private OrderManager orderManager; public void createOrder(OrderCreateDTO createDTO){ query(); validate(); orderManager.saveData(createDTO); } } @Service public class OrderManager{ @Autowired private OrderDao orderDao; @Transactional(rollbackFor = Throwable.class) public void saveData(OrderCreateDTO createDTO){ orderDao.saveData(createDTO); }}Copy the code
  1. Start class addition@EnableAspectJAutoProxy(exposeProxy = true), used within the methodAopContext.currentProxy()Get the proxy class and use transactions.
SpringBootApplication.java

@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class SpringBootApplication {}
Copy the code
OrderService.java
  
public void createOrder(OrderCreateDTO createDTO){
    OrderService orderService = (OrderService)AopContext.currentProxy();
    orderService.saveData(createDTO);
}
Copy the code

summary

Using the @Transactional annotation is certainly convenient during development, but long transactions can be a problem if you don’t pay attention to them. For complex business logic, I recommend using programmatic transactions to manage transactions. Of course, if you must use @Transactional, you can split your methods according to the two schemes mentioned above.