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 JdbcTemplate
example. Using other DB
tools 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()});
// ....其他操作...
}
}
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:
Check out the database:
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 JdbcTemplate
of update(String sql, @Nullable Object... args)
: the method
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)
:
The method is mainly used here execute(StatementCallback<T> action)
, enter this method:
It can be seen here DataSourceUtils.getConnection
that the database connection is obtained through the method, and enter this method:
TransactionSynchronizationManager
Is it a bit familiar to see here ? @Transactional
When explaining the source code analysis of declarative transactions earlier in this column, the logic of starting the transaction is to use TransactionSynchronizationManager
the 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 String
the ecology, access to database connections is basically used by default TransactionSynchronizationManager
.
Let's also look at @Transactional
the logic of obtaining the connection when the transaction is started under the annotation, under the DataSourceTransactionManager
following doGetTransaction
method:
You can see that it is also used here TransactionSynchronizationManager
to get the connection.
Let's see TransactionSynchronizationManager
what you have done and enter getResource
the method:
Here the method is triggered again doGetResource
, and enters under this method:
Here it is obviously obtained resources
from , let's see resources
what is in the end:
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 ThreadLocal
we ThreadLocal
opened 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 ThreadLocal
the 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
, Api
you can use to obtain the connection handle:
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
Among them key
is 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:
An exception has occurred, check the database:
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:
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. join
If 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:
Check out the database:
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 ThreadLocal
it 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();
}
}