Abstract:

Using worker threads in @Transactional requires solving three major problems:

  1. @TransactionalMultithreading is not supported;
  2. @TransactionalThe worker thread in cannot get the main thread modification.
  3. @TransactionalThe worker thread in the.

This article uses AOP+Spring events to present a practice for addressing these three issues.

Transactional annotation @transactional

As you know, it’s easy to use transactions in Spring Boot, using the @Transactional annotation on functions or classes. On the other hand, for time-consuming and network IO operations, the worker thread is generally used to process time-consuming and network IO so as not to block the main thread.

Consider the following business scenario: A Web service that receives the updatePerson request for a person update has the following two main business operations

@Transactional
public void updatePerson(String personCode) {
   // 1. Update the database with new personnel information.// 2. Synchronize the new staff information to Elasticsearch
   esService.syncToEs(personCode);
}
Copy the code

The first step updates only the database table T_PERSON. The second step of synchronization to Elasticsearch is a time-consuming operation. It will read multiple tables, T_person, T_company, t_social_relation, and send them to ES for indexing.

Now you need to optimize the execution time of the updatePerson function to support real-time updates. Esservice.synctoes (personCode); Put it into the worker thread.

However, this operation faces three major problems:

  1. @TransactionalMultithreading is not supported
  2. @TransactionalThe worker thread in the
  3. @TransactionalThe worker thread in the

@TransactionalTo start the worker thread

1. @TransactionalMultithreading is not supported

Spring Boot says @Transactional

This annotation commonly works with thread-bound transactions managed by org.springframework.transaction.PlatformTransactionManager, exposing a transaction to all data access operations within the current execution thread. Note: This does NOT propagate to newly started threads within the method.

That is, @transactional is thread-bound and only valid for the current thread. Transactional @Transactional makes no Transactional guarantees (consistency, isolation, atomicity, persistence) for operations in worker threads.

SyncToEs (personCode) in the worker thread. It is perfectly possible to start a new transaction, let’s call it a worker thread transaction. Updating staff information to the database and synchronizing ES can be split into two actions, the main thread transaction and the worker thread transaction are two separate transactions.

But it’s not that simple. This leads to the second problem: the worker thread cannot get the main thread modification.

2. @TransactionalThe worker thread in the

Since the worker thread is started while the main thread transaction is running, that is, the main thread transaction has not committed and the worker thread transaction has been started. Then, transactions in the worker thread cannot see changes to the main thread transaction (assuming the database isolation level is at commit reads and above).

Therefore, worker thread transactions need to be started after the main thread transaction has finished. For example, the following transformation

public void updatePerson(String personCode) {
   // 1. Update the database with new personnel information
   updatePersonMainProcess(persoCode);
   // 2. Synchronize the new staff information to Elasticsearch
   esService.syncToEs(personCode);
}

@Transactional
private void updatePersonMainProcess(String personCode) {
   // 1. Update the database with new personnel information. }public class EsService {
    @Async
    @Transactional
    public void syncToEs(String personCode) {
       // 2. Synchronize the new staff information to Elasticsearch. }}Copy the code

However, there is a third problem: worker threads in @Transactional can be lost hereafter

3. @TransactionalThe worker thread in the

In Spring Boot, there is a context, such as the Web request context RequestContextHolder, which is implemented internally using ThreadLocal, so the RequestContextHolder is also thread-bound. When the worker thread is enabled, the context of the Web request is lost.

Of course, RequestContextHolder also provides setRequestAttributes(@nullable RequestAttributes attributes, Boolean inheritable) methods that can be inherited by child threads. Reset the existing properties before starting the worker thread.

RequestContextHolder.setRequestAttributes(currentRequestAttributes(), true)
Copy the code

However, in the thread pool + RequestContextHolder. SetRequestAttributes (@ Nullable RequestAttributes attributes, Boolean inheritable) is invalid.

The reason is that RequestContextHolder’s parent thread inherits the context via InheritableThreadLocal. However, InheritableThreadLocal cannot work on threads in the thread pool.

We go further because InheritableThreadLocal copies the context of the parent thread when the child thread is created (which is implemented with a delay to the GET method). Threads in the thread pool are reused (threads are not recreated), so the parent thread context cannot be copied over.

The solution

The problem of parent and child thread context transmission in thread pool can be solved by using the TransmittableThreadLocal package Github address of Alibaba.

Simple introduction: Use the class TransmittableThreadLocal to save values and pass them across the thread pool.

Sample code:

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

// Set in the parent thread
context.set("value-set-in-parent");

/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

// Can be read in child thread, value is "value-set-in-parent"
String value = context.get();

Copy the code

@TransactionalTo gracefully start the worker thread

Summarize the additional operations required to start a worker thread in @Transactional

  1. The worker thread needs to start running after the main thread transaction ends
  2. The main thread context needs to be passed to the worker thread

The following is a practice that uses AOP+Spring events to start a worker thread.

  • Implement a Spring event sending class that is calledpublishWhen an event is sent, it is not sent directly, but stored in context. After the Controller Controller returns successfully, it retrieves the event saved in the context and starts the worker thread to send the event.

Working logic:

  1. PersonServiceInstead of performing synchronous es directly inAfterRequestEventPublisherThrows a people information change event
  2. AfterRequestEventPublisherTo achieve theApplicationEventPublisherInterface, itspublishThe method is rewritten to: Save the event to the context
  3. AfterRequestEventPublisherMade a section, after the implementation of return notificationafterReturningAdvice, intercepts all controllers, where the worker thread is enabled to send events. When the controller returns, the Spring call returns a notification afterwardsAfterRequestEventPublisher.afterReturningAdvice.
public class PersonService {
    // Spring event notifier, which is responsible for starting the worker thread to send the event when the Controller returns
    @Resource
    private AfterRequestEventPublisher afterRequestEventPublisher;
    @Transactional
    public void updatePerson(String personCode) {
       // 1. Update the database with new personnel information.// 2. Throw an event to inform Elasticsearch to synchronize the new staff information
       afterRequestEventPublisher.publish(new PersonEditEvent(PersonCode))
    }
}

public class AfterRequestEventPublisher implements ApplicationEventPublisher {
    // Spring Boot's default event notification
    @Resource
    private ApplicationEventPublisher publisher;
    // The pool of worker threads
    @Resource
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
    /** * collect the event, and enable the worker thread to send the event asynchronously at the end of the request
    @Override
    public void publishEvent(Object event) {
        List<Object> eventObjects = (List<Object>) RequestContextHolder.currentRequestAttributes()
                .getAttribute("AfterRequestEventPublisher", RequestAttributes.SCOPE_REQUEST);
        if (Objects.isNull(eventObjects)) {
            eventObjects = new ArrayList<>();
            eventObjects.add(event);
            RequestContextHolder.currentRequestAttributes()
                    .setAttribute("AfterRequestEventPublisher", eventObjects, RequestAttributes.SCOPE_REQUEST);
        } else{ eventObjects.add(event); }}/** * Intercepts all controller methods until the controller ends
    @AfterReturning(pointcut = "execution(public * org.example.controller.*Controller.*(..) )", returning = "retVal")
    public void afterReturningAdvice(Object retVal) {
        final List<Object> eventObjects = (List<Object>) RequestContextHolder.currentRequestAttributes()
                .getAttribute("AfterRequestEventPublisher", RequestAttributes.SCOPE_REQUEST);
        if (CollectionUtils.isEmpty(eventObjects)) {
            return;
        }
       // The context is passed to the worker thread, where the event is sent. The reason for not using @async here is to leave some thread customization for the future. Such as more elegant context delivery.
       // There is no TransmittableThreadLocal
        RequestContextHolder.setRequestAttributes(RequestContextHolder.currentRequestAttributes(), true);
        threadPoolTaskExecutor.execute(() -> {
            for(Object event : eventObjects) { publisher.publishEvent(event); }}); }}public class EsService {
    // Receives the PersonEditEvent event, which is executed in the worker thread
    @EventListener(PersonEditEvent.class)
    @Transactional
    public void syncToEs(PersonEditEvent event) {
        // synchronize to es}}Copy the code

conclusion

The prerequisites for using a worker thread in Spring Boot’s @Transactional annotation are:

  1. The main thread flow can be split into two separate transactions.

    In this case, if the logic of the ES operation is synchronized only to the ES, the operation cannot be performed in the worker thread. Because worker thread transactions cannot sense whether the main thread transaction committed successfully. If the main thread transaction rolls back, and the worker thread still synchronizes the changes to ES, then the database is inconsistent with ES. Similarly, if the main thread commits successfully and the worker thread rolls back, the database will be inconsistent with es.

Problems with using worker threads in Spring Boot’s @Transactional annotation:

  1. @transactional does not support multithreading;

    The main thread and worker thread are required to use two separate transactions.

  2. A worker thread in @Transactional cannot get mainthread changes;

    The worker thread is required to complete the main thread transaction before starting execution.

  3. A worker thread in @Transactional will be lost below.

    Requires passing context. The TransmittableThreadLocal is used in the Github address of the transmittable-threadlocal package to save the value and pass it across the thread pool.