Like it first, then watch it, develop a good habit
Transaction Management , a topic that is said to be bad and bad, is also one of the basic stocks in the eight-legged essay. But in addition to the communication behaviors that need to be read and recited in the eight-part essay, the "why" and core principles behind it are more important.
Before writing this article, I have also reviewed some articles about the principle of transaction manager, but most of them are blunt translation of the source code, and keep annotating the source code. Although this kind of source code translation article is helpful, it is not a good experience for readers. It is easy to fall into some details of the code and cannot help readers quickly understand the overall picture of transaction management and design ideas.
This article will analyze the design ideas of Spring transaction management step by step from a design perspective (I will design a transaction manager, can I still use it?)
Why do you need transaction management?
Let’s take a look at what should do wants multiple operations (methods/classes) in one transaction:
// MethodA:
public void methodA(){
Connection connection = acquireConnection();
try{
int updated = connection.prepareStatement().executeUpdate();
methodB(connection);
connection.commit();
}catch (Exception e){
rollback(connection);
}finally {
releaseConnection(connection);
}
}
// MethodB:
public void methodB(Connection connection){
int updated = connection.prepareStatement().executeUpdate();
}
Or use ThreadLocal to store Connection?
static ThreadLocal<Connection> connHolder = new ThreadLocal<>();
// MethodA:
public void methodA(){
Connection connection = acquireConnection();
connHolder.set(connection);
try{
int updated = connection.prepareStatement().executeUpdate();
methodB();
connection.commit();
}catch (Exception e){
rollback(connection);
}finally {
releaseConnection(connection);
connHolder.remove();
}
}
// MethodB:
public void methodB(){
Connection connection = connHolder.get();
int updated = connection.prepareStatement().executeUpdate();
}
Still a little disgusting, let's abstract a bit? Extract the operation of binding Connection as a public method:
static ThreadLocal<Connection> connHolder = new ThreadLocal<>();
private void bindConnection(){
Connection connection = acquireConnection();
connHolder.set(connection);
}
private void unbindConnection(){
releaseConnection(connection);
connHolder.remove();
}
// MethodA:
public void methodA(){
try{
bindConnection();
int updated = connection.prepareStatement().executeUpdate();
methoB();
connection.commit();
}catch (Exception e){
rollback(connection);
}finally {
unbindConnection();
}
}
// MethodB:
public void methodB(){
Connection connection = connHolder.get();
int updated = connection.prepareStatement().executeUpdate();
}
It looks better now, but I have a new requirement: I want methodB to be a new transaction independently, commit and rollback separately, without affecting methodA
This...but it's a bit difficult. A Connection is already bound to ThreadLocal, and it won't be easy to handle a new transaction.
If it is more complicated, methodC needs to be called in methodB, and methodC also needs an independent transaction...
Moreover, every time the operation of bind/unbind is a bit too silly, in case any method forgot to write unbind, it is not over if a connection leaks at the end!
Fortunately, Spring provides a transaction manager to help us solve this series of pain points.
What problem does Spring transaction management solve?
The transaction management provided by Spring can help us manage transaction-related resources, such as JDBC Connection, Hibernate Session, and Mybatis SqlSession. For example, the above Connection is bound to ThreadLocal to solve this way of sharing a transaction, Spring transaction management has already done it for us.
It can also help us deal with nested transactions in complex scenarios, such as the methodB/methodC independent transactions mentioned earlier.
What is a nested transaction?
Take the above example as an example. MethodB is called in methodA. Both methods have operations on the database, and both require transactions:
// MethodA:
public void methodA(){
int updated = connection.prepareStatement().executeUpdate();
methodB();
// ...
}
// MethodB:
public void methodB(){
// ...
}
scenario where there are transactions in multiple method call chains is nested transactions. However, it should be noted that it does not mean that multiple methods use one transaction to call nesting, even if it is a different transaction, as long as the method's call chain is a nested transaction.
What is transaction propagation behavior?
Should the sub-method in the call chain use a new transaction or the current transaction? This sub-method determines whether to use a new transaction or the current transaction (or not to use a transaction) strategy, which is called transaction propagation.
In Spring's transaction management, the transaction processing strategy of this sub-method .
What kind of transaction spread behavior?
Spring's transaction management supports a variety of communication behaviors, so I won't post them here, and there are everything in the stereotypes.
But after classifying these transmission behaviors, there are nothing more than the following three:
- Prioritize the use of current transactions
- Do not use the current transaction, create a new transaction
- Do not use any transactions
For example, in the above example, methodB/methodC independent transactions belong to the second propagation behavior-do not use the current transaction, create a new transaction
Look at a chestnut
Take the Spring JDBC + Spring annotation version transaction as an example. In the default transaction propagation behavior, methodA and methodB will use the same Connection, in a transaction
@Transactional
public void methodA(){
jdbcTemplate.batchUpdate(updateSql, params);
methodB();
}
@Transactional
public void methodB(){
jdbcTemplate.batchUpdate(updateSql, params);
}
What if I want methodB not to use methodA's transaction, and create a new connection/transaction by myself? Just simply configure the @Transactional annotation:
@Transactional
public void methodA(){
jdbcTemplate.batchUpdate(updateSql, params);
methodB();
}
// 传播行为配置为 - 方式2,不使用当前事务,独立一个新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
jdbcTemplate.batchUpdate(updateSql, params);
}
It's as simple as that. Obtaining Connection/Multi-Method Sharing Connection/Multi-Method Sharing+Exclusive Connection/Submit/Release Connections and other operations do not need us to worry about, Spring does it for us.
How to roll back?
In the transaction management of the annotation version, the default rollback strategy is: roll back when an exception is thrown. This default strategy is quite good, even the rollback helped us solve it, and there is no need to manually roll back.
But what if in a nested transaction, the child method is independent of the new transaction? Even if an exception is thrown at this time, only the sub-transaction can be rolled back, and the previous transaction cannot be directly affected.
But if the thrown exception is not caused by sql, such as the verification fails or other exceptions, should the current transaction be rolled back at this time?
This is not necessarily true. Who says that if an exception is thrown, it is necessary to roll back. Can it be done without an exception?
of course can! Throwing exceptions and rolling back transactions are originally two problems, which can be connected together or handled separately
// 传播行为配置为 - 方式2,不使用当前事务,独立一个新事务
// 指定 Exception 也不会滚
@Transactional(propagation = Propagation.REQUIRES_NEW, noRollbackFor = Exception.class)
public void methodB(){
jdbcTemplate.batchUpdate(updateSql, params);
}
Each transaction/connection uses a different configuration
In addition to propagation and rollback, you can also use different configurations for each transaction/connection, such as different isolation levels:
@Transactional
public void methodA(){
jdbcTemplate.batchUpdate(updateSql, params);
methodB();
}
// 传播行为配置为 - 方式2,不使用当前事务,独立一个新事务
// 这个事务/连接中使用 RC 隔离级别,而不是默认的 RR
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED)
public void methodB(){
jdbcTemplate.batchUpdate(updateSql, params);
}
In addition to the isolation level, other JDBC Connection configurations are also supported, such as readOnly. In this way, although we do not need to display the connection/session, we can still configure different parameters for each transaction in the nest, which is very flexible.
Function summary
Okay, now that you understand all the core functions of Spring transaction management, let's summarize these core functions:
- Connection/resource management-no need to manually acquire, share, and release resources
- Support for nested transactions-support the use of different resource strategies and rollback strategies in nested transactions
- Each transaction/connection uses a different configuration
Transaction Manager (TransactionManager) model
In fact, think about it carefully, there are only two core operations of transaction management: commit and rollback. The so-called propagation, nesting, and rollback are all based on these two operations.
Therefore, Spring abstracts the core function of Transaction Manager (Transaction Manager) . Based on the core of this transaction manager, a variety of transaction management methods can be implemented.
This core transaction manager has only three functional interfaces:
- Get transaction resource , the resource can be any, such as jdbc connection/hibernate mybatis session, etc., and then bind and store
- commit transaction -commit the specified transaction resource
- rollback transaction -rollback specified transaction resource
interface PlatformTransactionManager{
// 获取事务资源,资源可以是任意的,比如jdbc connection/hibernate mybatis session之类
TransactionStatus getTransaction(TransactionDefinition definition)
throws TransactionException;
// 提交事务
void commit(TransactionStatus status) throws TransactionException;
// 回滚事务
void rollback(TransactionStatus status) throws TransactionException;
}
Transaction Definition-TransactionDefinition
Remember the @Transactional annotation above, which defines properties such as propagation behavior, isolation level, rollback strategy, and read-only. This is the definition of a transaction operation.
When acquiring transaction resources, different configurations need to be made according to the definition of the transaction:
- For example, if a new transaction is configured, a new transaction resource needs to be created instead of the existing transaction resource.
- For example, if the isolation level is configured, when the resource (Connection) is created for the first time, it is necessary to set the propagation for the Connection
- For example, if the read-only property is configured, then when the resource (Connection) is created for the first time, readOnly needs to be set for the Connection
Why use a TransactionDefinition alone to store transaction definitions, can't you directly use annotated attributes?
Of course you can, but the transaction management of annotations is only the automatic transmission provided by Spring, as well as manual transmission transaction management suitable for old drivers (described later); annotations are not available for manual transmission, so a separate transaction definition model is built, so that it can be realized Universal.
Transaction Status-TransactionStatus
Since the transaction of each sub-method may be different under nested transactions, there must be a sub-method transaction status-TransactionStatus, which is used to store some data and status of the current transaction, such as transaction resources (Connection), rollback status, etc. .
Obtain transaction resources
The first step of the transaction manager is to obtain/create resources according to the transaction definition. The most troublesome thing in this step is to distinguish the propagation behavior. The logic under different propagation behaviors is different.
"Under the default propagation behavior, use the current transaction", how does it count as a current transaction?
Save the transaction resource, as long as it already exists, it means there is a current transaction, just get the stored transaction resource directly. The example at the beginning of the article also demonstrates that if you want multiple methods to use the same transaction insensibly, you can use ThreadLocal to store it, which is simple and rude.
Spring does the same, but it is more complicated to implement. It abstracts a layer of transaction resource synchronization manager-TransactionSynchronizationManager (referred to as TxSyncMgr later . In this synchronization manager, ThreadLocal is used to store transaction resources (this article is for It is easy to understand, and non-critical source code is not posted as much as possible).
The rest is to implement different strategies according to different propagation behaviors. After classification, there are only 3 conditional branches:
- There are current affairs-processing is different according to different propagation behaviors
- There is no transaction at present, but a new transaction needs to be opened
- No business at all-this is rarely used
public final TransactionStatus getTransaction(TransactionDefinition definition) {
//创建事务资源 - 比如 Connection
Object transaction = doGetTransaction();
if (isExistingTransaction(transaction)) {
// 处理当前已有事务的场景
return handleExistingTransaction(def, transaction, debugEnabled);
}else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED){
// 开启新事务
return startTransaction(def, transaction, debugEnabled, suspendedResources);
}else {
// 彻底不用事务
}
// ...
}
First introduce branch 2-there is no transaction at present, but a new transaction needs to be opened. The logic is relatively simple. Just create a new transaction resource and bind it to ThreadLocal:
private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction,
boolean debugEnabled, SuspendedResourcesHolder suspendedResources) {
// 创建事务
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
// 开启事务(beginTx或者setAutoCommit之类的操作)
// 然后将事务资源绑定到事务资源管理器 TransactionSynchronizationManager
doBegin(transaction, definition);
Now go back to the branch 1-there are currently transactions- handled differently according to different propagation behaviors. This is a little troublesome. Because there are sub-methods that require independent transactions, TransactionSynchronizationManager can only store one transaction resource.
Suspend and Resume
Spring uses a Suspend-Resume design to solve this nested resource processing problem. When the sub-method needs an independent transaction, the current transaction is suspended, the current transaction resource is removed from TxSyncMgr, and the state of the new transaction is created, the suspended transaction resource is saved to the new transaction status TransactionStatus; at the end of the sub-method At this time, you only need to take out the suspended transaction resources from the transaction state of the sub-method again, and re-bind it to TxSyncMgr to complete the recovery operation.
The entire suspend-resume process is shown in the following figure:
Note: The suspend operation is done at the step of obtaining transaction resources, and the resume operation is done at the end of the sub-method (commit or rollback).
In this way, each TransactionStatus will save the pending pre-transaction resources. If the method call chain is very long and each time is a new transaction, then this TransactionStatus will look like a linked list:
Commit transaction
After obtaining resources and completing the operation, it comes to the step of committing the transaction. This commit operation is relatively simple, with only two steps:
- Commit only when the current transaction is new
- Handling pending resources
How do you know that it is a new business?
After each transaction nesting, a new TransactionStatus will be created. In this transaction status, it will be recorded whether the current transaction is a new transaction. If multiple sub-methods all use a transaction resource, none of them are new transactions except the TransactionStatus that creates the transaction resource first.
As shown in the figure below, when A -> B -> C, because BC all use the current transaction, although the transaction resources used by ABC are the same, only A's TransactionStatus is a new transaction, and BC is not; then commit at BC During the transaction, the commit operation will not be actually called. Only when the commit operation is returned to A, the commit operation will be called.
Here's another explanation, why a new transaction needs to be committed, but there is already a transaction but nothing needs to be done:
Because for the new transaction, the commit operation here is the transaction completed; for the non-new transaction scenario, the pre-transaction (that is, the current transaction) has not been executed yet, and there may be other database operations later, so this commit The operation has to be done by the creator of the current transaction, and it cannot be submitted here.
Rollback transaction
In addition to commit, there is also rollback. The logic of rolling back a transaction is similar to that of committing a transaction:
- If it is a new transaction to roll back, the reasons have been introduced above
- If it is not a new transaction, only the rollback flag is set
- Handling pending resources
Note: The transaction manager does not include the rollback strategy. The rollback strategy is an enhanced function of the AOP version of transaction management, but this function does not belong to the core transaction manager
Automatic and manual transmission
Spring's transaction management functions are all running around the above transaction manager, providing three ways to manage transactions, namely:
- XML AOP transaction management-relatively old and not used much now
- Annotated version of transaction management-@Transactional
- TransactionTemplate-manual transaction management, also known as programmatic transaction management
Automatic transmission
XML/@Transactional two types of annotation management based on AOP, the entry class is TransactionInterceptor, which is an AOP Interceptor, responsible for calling the transaction manager to implement transaction management.
Because the core functions are implemented in the transaction manager, this AOP Interceptor is very simple, just call the transaction manager, the core (pseudo) code is as follows:
public Object invoke(MethodInvocation invocation) throws Throwable {
// 获取事务资源
Object transaction = transactionManager.getTransaction(txAttr);
Object retVal;
try {
// 执行业务代码
retVal = invocation.proceedWithInvocation();
// 提交事务
transactionManager.commit(txStatus);
} catch (Throwable ex){
// 先判断异常回滚策略,然后调用事务管理器的 rollback
rollbackOn(ex, txStatus);
}
}
And the automatic transaction management of AOP also adds a rollback strategy gameplay, which is not available in the manual transaction Template, but this function is not in the transaction manager, but an enhancement of the AOP version of the transaction.
Manual gear
TransactionTemplate is a manual transaction management. Although it is not convenient for annotations, it is flexible. You can control exceptions/rollbacks by yourself.
So this implementation is simpler. There is no exception rollback strategy. The special rollback method must be set by yourself (the default is that any exception will be rolled back). The core (pseudo) code is as follows:
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
// 获取事务资源
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
// 执行 callback 业务代码
result = action.doInTransaction(status);
}
catch (Throwable ex) {
// 调用事务管理器的 rollback
rollbackOnException(status, ex);
}
提交事务
this.transactionManager.commit(status);
}
}
Why is there such a convenient automatic transmission, but also manual transmission?
Because manual gear is more flexible, you can play as you want. For example, I can perform multiple database operations in one method, but use different transaction resources:
Integer rows = new TransactionTemplate((PlatformTransactionManager) transactionManager,
new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_UNCOMMITTED))
.execute(new TransactionCallback<Integer>() {
@Override
public Integer doInTransaction(TransactionStatus status) {
// update 0
int rows0 = jdbcTemplate.update(...);
// update 1
int rows1 = jdbcTemplate.update(...);
return rows0 + rows1;
}
});
Integer rows2 = new TransactionTemplate((PlatformTransactionManager) transactionManager,
new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_UNCOMMITTED))
.execute(new TransactionCallback<Integer>() {
@Override
public Integer doInTransaction(TransactionStatus status) {
// update 2
int rows2 = jdbcTemplate.update(...);
return rows2;
}
});
In the above example, through TransactionTemplate, we can precisely control the use of the same transaction resource and isolation level for update0/update1, while update2 uses a transaction resource alone, and does not require a new type of annotation method.
Is it okay to have a single hand?
Of course, as long as we are using the same instance of the transaction manager, because the operation of binding resources to the synchronization resource manager is performed in the transaction manager.
In the transaction management of the AOP version, the manual transaction management can also be used to continue the operation, and the same transaction resource can also be used.
For example, in the following code, update1/update2 is still in a transaction, and the transaction will not be committed after the callback of update2 ends. The transaction will finally be committed in TransactionInterceptor when methodA ends.
@Transactional
public void methodA(){
// update 1
jdbcTemplate.update(...);
new TransactionTemplate((PlatformTransactionManager) transactionManager,
new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_UNCOMMITTED))
.execute(new TransactionCallback<Integer>() {
@Override
public Integer doInTransaction(TransactionStatus status) {
// update 2
int rows2 = jdbcTemplate.update(...);
return rows2;
}
});
}
to sum up
The core of Spring's transaction management is an abstract transaction manager. Several methods of XML/@Transactional/TransactionTemplate are based on this transaction manager. The core implementation of the three methods is not very different, but the entrance is different.
In order to facilitate understanding, this article omits a large number of non-critical implementation details, which may lead to some imprecise descriptions. If you have any questions, please leave a message in the comment area.
Originality is not easy, unauthorized reprinting is prohibited. If my article is helpful to you, please like/favorite/follow to encourage and support it ❤❤❤❤❤❤
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。