From the perspective of source code, analyze the data exception rollback scheme under the condition of multi-thread concurrency

1. Data exception rollback solution in the case of multi-threaded concurrency

In the case of requiring multiple data operations without sequence, we can generally choose to use concurrent operations to improve the processing speed, but in concurrent cases, can we still solve the problem of transaction rollback @Transactional?

For example, the following table structure:

CREATE TABLE `test` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `thread_name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

If two write operations need to be performed, and there is no order of writing, we can open a thread to write concurrently. Here we take the operation as an JdbcTemplateexample. Using other DBtools has the same effect, for example:

@Service
public class TestService {
    
    

    @Resource
    JdbcTemplate jdbcTemplate;

    @Transactional(rollbackFor = Exception.class)
    public void test() {
    
    
        // 放入子线程
        CompletableFuture.runAsync(() -> {
    
    
            jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                    , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
        });
        // ....其他操作...
        jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
        // ....其他操作...
    }
}

insert image description here

The database has successfully written two pieces of data. If an exception occurs during other operations:

@Service
public class TestService {
    
    

    @Resource
    JdbcTemplate jdbcTemplate;

    @Transactional(rollbackFor = Exception.class)
    public void test() {
    
    
        // 放入子线程
        CompletableFuture.runAsync(() -> {
    
    
            jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                    , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
        });
        // ....其他操作...
        jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
        // ....其他操作...
        int a = 1 / 0;
    }
}

After running, you can see that an exception has been thrown:
insert image description here

Check out the database:

insert image description here

It is found that a piece of data is still written, and the operation in the thread is not rolled back, but the main thread is rolled back. Since one is rolled back and the other is not rolled back, it must not use the same database connection. Here is the source code to see where to get the database connection JdbcTemplate:

Enter the method JdbcTemplateof update(String sql, @Nullable Object... args): the method
insert image description here
of the current class is called update(String sql, @Nullable PreparedStatementSetter pss), and the method of the current class is finally called update(final PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss):
insert image description here

The method is mainly used here execute(StatementCallback<T> action), enter this method:

insert image description here

It can be seen here DataSourceUtils.getConnectionthat the database connection is obtained through the method, and enter this method:

insert image description here

TransactionSynchronizationManagerIs it a bit familiar to see here ? @TransactionalWhen explaining the source code analysis of declarative transactions earlier in this column, the logic of starting the transaction is to use TransactionSynchronizationManagerthe database connection obtained. If you don’t understand this part, you can read the following article:

SpringTx source code analysis - @Transactional declarative transaction execution principle

In fact, in Stringthe ecology, access to database connections is basically used by default TransactionSynchronizationManager.

Let's also look at @Transactionalthe logic of obtaining the connection when the transaction is started under the annotation, under the DataSourceTransactionManagerfollowing doGetTransactionmethod:

insert image description here

You can see that it is also used here TransactionSynchronizationManagerto get the connection.

Let's see TransactionSynchronizationManagerwhat you have done and enter getResourcethe method:

insert image description here

Here the method is triggered again doGetResource, and enters under this method:

insert image description here

Here it is obviously obtained resourcesfrom , let's see resourceswhat is in the end:

insert image description here

It is one ThreadLocal, do you understand now that in the absence of multi-threading, when the transaction is started, the obtained connection is put into the current , and other components perform data operations later, and the connection is also first obtained from , so that all operations are performed in one connection, and naturally it can be rolled back. Since ThreadLocalwe ThreadLocalopened the thread separately above, the operation in the thread tried to obtain the connection in progress, but could not obtain it, so we could only obtain a new connection operation, resulting in the inconsistency between the connection when declaring the transaction and the connection during the actual operation, so it cannot be rolled back ThreadLocal.

Now that we have found the cause of the problem, how can we solve it?

Since ThreadLocalthe connection is different due to , when we start the thread, we will add certain information to it. It is used to obtain the connection TransactionSynchronizationManager, so it is also used to add it TransactionSynchronizationManager. By observing TransactionSynchronizationManager, Apiyou can use to obtain the connection handle:

ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);

Among them keyis the current data source, the binding handle can be used:

 TransactionSynchronizationManager.bindResource(dataSource, conHolder);

The removal handle can be used:

TransactionSynchronizationManager.unbindResource(dataSource);

Let's modify the previous program:

@Service
public class TestService {
    
    

    @Resource
    JdbcTemplate jdbcTemplate;

    @Resource
    DataSource dataSource;

    @Transactional(rollbackFor = Exception.class)
    public void test() {
    
    
        // 获取当前线程的句柄
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        // 放入子线程
        CompletableFuture.runAsync(() -> {
    
    
            // 子线程绑定
            TransactionSynchronizationManager.bindResource(dataSource, conHolder);
            jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                    , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
            // 解绑
            TransactionSynchronizationManager.unbindResource(dataSource);
        });
        // ....其他操作...
        jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
        // ....其他操作...
        int a = 1 / 0;
    }
}

run again:

insert image description here

An exception has occurred, check the database:

insert image description here

The data was rolled back successfully!

Can the fake exception appear in the child thread and can be rolled back? Let's start the experiment:

@Service
public class TestService {
    
    

    @Resource
    JdbcTemplate jdbcTemplate;

    @Resource
    DataSource dataSource;

    @Transactional(rollbackFor = Exception.class)
    public void test() {
    
    
        // 获取当前线程的句柄
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        // 放入子线程
        CompletableFuture.runAsync(() -> {
    
    
            // 子线程绑定
            TransactionSynchronizationManager.bindResource(dataSource, conHolder);
            jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                    , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
            int a = 1 / 0;
            // 解绑
            TransactionSynchronizationManager.unbindResource(dataSource);
        });
        // ....其他操作...
        jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
        // ....其他操作...
    }
}

After running, view the data:

insert image description here
It is found that there is no rollback phenomenon. This is because the exception is in the child thread Runnable, and the parent thread does not perceive the exception. How to make the parent thread perceive it? We can add one at the end of data processing. joinIf there is another exception, it will be thrown to the parent thread:

@Service
public class TestService {
    
    

    @Resource
    JdbcTemplate jdbcTemplate;

    @Resource
    DataSource dataSource;

    @Transactional(rollbackFor = Exception.class)
    public void test() {
    
    
        // 获取当前线程的句柄
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        // 放入子线程
        CompletableFuture future = CompletableFuture.runAsync(() -> {
    
    
            // 子线程绑定
            TransactionSynchronizationManager.bindResource(dataSource, conHolder);
            jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                    , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
            int a = 1 / 0;
            // 解绑
            TransactionSynchronizationManager.unbindResource(dataSource);
        });
        // ....其他操作...
        jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
        // ....其他操作...
        future.join();
    }
}

After running, you can see that an exception has been thrown:

insert image description here

Check out the database:

insert image description here

The data was also successfully rolled back.

2. Extension: MVC sub-thread obtains Request information

After reading the above transaction process, in the same way, in MVC, if it is originally running in the main thread, there is a need to optimize it in the sub-thread later, but there is information obtained from ThreadLocalit Request:

@RestController
@RequestMapping("/test3")
public class RequestController {
    
    

    @GetMapping("/test")
    public void test(){
    
    
        // 获取句柄
        ServletRequestAttributes att = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        CompletableFuture.runAsync(()->{
    
    
            // 绑定
            RequestContextHolder.setRequestAttributes(att);

            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                    .getRequestAttributes();

            HttpServletRequest request = attributes.getRequest();
            System.out.println(request.getHeader("token"));
            // 解绑
            RequestContextHolder.resetRequestAttributes();
        }).join();
    }
}

Guess you like

Origin blog.csdn.net/qq_43692950/article/details/130914163