Hello everyone, I am the class representative.
attention to my public account: 16136c0ab2434f Java class representative , the original dry goods are first released, waiting for you.
1 Introduction
Spring's declarative transaction greatly facilitates the writing of daily transaction-related code. Its design is so ingenious that it is almost impossible to feel its existence in use. You only need to elegantly add an @Transactional annotation, and everything is logical. finished!
It is no exaggeration to say that Spring's declarative transactions are so easy to use that most people forget how to write programmatic transactions.
However, the more you think it should be, the harder it is to troubleshoot if something goes wrong. I don't know if you and your friends have encountered @Transactional failure scenarios. This is not only a pit often stepped on in daily development, but also a high-frequency problem in interviews.
In fact, these failure scenarios do not need to be memorized by rote. If you understand its working principle and combine the source code, you can analyze it yourself when you need it by Debug. After all, the source code is the best manual.
Again, teaching people how to fish is worse than teaching people how to fish. Even if the class representative summarizes 100 failure scenarios, it may not cover the pits you may step on. Therefore, in this article, the class representative will combine several common failure situations to explain the reasons for the failure from the source code level. After reading this article carefully, I believe you will have a deeper understanding of declarative affairs.
All the code in the article has been uploaded to the github class representative. In order to facilitate rapid deployment and operation, the sample code uses the in-memory database H2
, and no additional database environment is required.
2. Review handwriting affairs
The transaction at the database level has four characteristics of ACID, which together ensure the accuracy of the data in the database. The principle of transactions is not the focus of this article. We only need to know that the H2 database used in the sample fully implements the support for transactions (read committed).
When writing Java code, we use the JDBC interface to interact with the database to complete transaction related instructions. The pseudo code is as follows:
//获取用于和数据库交互的连接
Connection conn = DriverManager.getConnection();
try {
// 关闭自动提交:
conn.setAutoCommit(false);
// 执行多条SQL语句:
insert();
update();
delete();
// 提交事务:
conn.commit();
} catch (SQLException e) {
// 如果出现异常,回滚事务:
conn.rollback();
} finally {
//释放资源
conn.close();
}
This is a typical programmatic transaction code flow: turn off auto-commit before starting, because by default, auto-commit is turned on, and each statement will open a new transaction, which will be automatically submitted after execution.
Turn off the automatic commit of the transaction to allow multiple SQL statements to be in the same transaction. When the code runs normally, the transaction is committed, and if an exception occurs, the entirety is rolled back to ensure the integrity of multiple SQL statements.
In addition to transaction submission, the database also supports the concept of savepoints. In a physical transaction, multiple savepoints can be set to facilitate rollback to the specified savepoint (it is similar to the savepoint when playing a stand-alone game, and you can do so at any time after the character hangs up. Back to the last archive) The code to set and roll back to the save point is as follows:
//设置保存点
Savepoint savepoint = connection.setSavepoint();
//回滚到指定的保存点
connection.rollback(savepoint);
//回滚到保存点后按需提交/回滚前面的事务
conn.commit();//conn.rollback();
The work done by Spring's declarative transaction centers around the two pairs of commands committing/rolling back transactions and setting/rolling back to savepoint. In order to allow us to write as little code as possible, Spring defines several propagation attributes to further abstract the transaction. Note that Spring's transaction propagation (Propagation) is just an abstraction defined by Spring, and has nothing to do with the database, so don't confuse it with the database's transaction isolation level.
3. Spring's transaction propagation (Transaction Propagation)
Observe the traditional transaction code:
conn.setAutoCommit(false);
// 执行多条SQL语句:
insert();
update();
delete();
// 提交事务:
conn.commit();
This code expresses that three SQL statements are in the same transaction.
They may be different methods in the same class, or they may be different methods in different classes. How to express concepts such as adding other transactions, creating your own transactions, nesting transactions, etc. in transaction methods? This depends on Spring's transaction propagation mechanism.
Transaction propagation (Transaction Propagation) is literally: the propagation/delivery method of the transaction.
TransactionDefinition
interface of the Spring source code, 7 kinds of propagation attributes are defined. The official website explains 3 of them. As long as we understand these 3, the remaining 4 are just analogy.
1)PROPAGATION_REQUIRED
Literal meaning: spread-must
PROPAGATION_REQUIRED
is its default propagation attribute, forcibly open the transaction, if the previous method has opened the transaction, then join the previous transaction, the two physically belong to the same transaction.
A picture is worth a thousand words. The following picture shows that they are physically in the same transaction:
The above picture is translated into pseudo code like this:
try {
conn.setAutoCommit(false);
transactionalMethod1();
transactionalMethod2();
conn.commit();
} catch (SQLException e) {
conn.rollback();
} finally {
conn.close();
}
Since it is in the same physical transaction, if transactionalMethod2()
and it needs to be rolled back, should transactionalMethod1()
also be rolled back?
Thanks to the above diagram and pseudo code, we can easily get the answer, transactionalMethod1()
must be rolled back.
Here is a question:
The exception in the transaction method is eaten by try catch, can the transaction be rolled back?
Don't rush to conclusions, look at the following two code examples.
Example 1: The situation that will not be rolled back (transaction failure)
Observe the following code, methodThrowsException()
did nothing but throw an exception, and the caller try catch
the exception 06136c0ab245dc thrown. In this scenario, the rollback will not be triggered.
@Transactional(rollbackFor = Exception.class)
public void tryCatchRollBackFail(String name) {
jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
try {
methodThrowsException();
} catch (RollBackException e) {
//do nothing
}
}
public void methodThrowsException() throws RollBackException {
throw new RollBackException(ROLL_BACK_MESSAGE);
}
Example 2: A rollback situation (transaction takes effect)
Look at this example again, the same try catch exception, but the result is completely opposite
@Transactional(rollbackFor = Throwable.class)
public void tryCatchRollBackSuccess(String name, String anotherName) {
jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
try {
// 带事务,抛异常回滚
userService.insertWithTxThrowException(anotherName);
} catch (RollBackException e) {
// do nothing
}
}
@Transactional(rollbackFor = Throwable.class)
public void insertWithTxThrowException(String name) throws RollBackException {
jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
throw new RollBackException(ROLL_BACK_MESSAGE);
}
propagation
attribute is not set for the transactions of the two methods, and the default is PROPAGATION_REQUIRED
. That is, the former starts the transaction, the latter joins the previously opened transaction, and both belong to the same physical transaction. insertWithTxThrowException()
method throws an exception, marking the transaction for rollback. Since everyone is in the same boat, the latter overturned the boat, and the former is certainly not immune.
Therefore tryCatchRollBackSuccess()
will also be rolled back, and the results can be viewed by executing this use case
Visit http://localhost:8080/h2-console/ , the connection information is as follows:
Click Connect
enter the console to view the data in the table:
The USER table does not insert data, which proves our conclusion, and you can see that the log reports an error:
Transaction rolled back because it has been marked as rollback-only
transaction has been rolled back because it was marked as must be rolled back.
That is, the transaction triggered by the latter method is rolled back, so that the insertion of the previous method is also rolled back.
Seeing this, you should be able to PROPAGATION_REQUIRED
thoroughly. In this example, the two methods are in the same physical transaction, which affects each other and rolls back.
You may ask, what should I do if I want the two methods that have started the transaction to not affect each other?
This will use the type of propagation described below.
2)、PROPAGATION_REQUIRES_NEW
Literal meaning: spread-must-new
PROPAGATION_REQUIRES_NEW
difference between 06136c0ab24718 and PROPAGATION_REQUIRED
is that it always opens an independent transaction and will not participate in an existing transaction, which ensures that the status of the two transactions is independent of each other, does not affect each other, and will not interfere with the rollback of one party. To the other party.
A picture is worth a thousand words. The following picture shows that they are not physically in the same transaction:
The above picture is translated into pseudo code like this:
//Transaction1
try {
conn.setAutoCommit(false);
transactionalMethod1();
conn.commit();
} catch (SQLException e) {
conn.rollback();
} finally {
conn.close();
}
//Transaction2
try {
conn.setAutoCommit(false);
transactionalMethod2();
conn.commit();
} catch (SQLException e) {
conn.rollback();
} finally {
conn.close();
}
TransactionalMethod1
new transaction. When he calls TransactionalMethod2
, which also needs a transaction, because the propagation attribute of the latter is set to PROPAGATION_REQUIRES_NEW
, the previous transaction is suspended (as for how to suspend, we will see from the source code later) and start a physical On a new transaction independent of the former, the rollback of the two transactions will not interfere with each other.
Still the previous example, just set the transaction propagation property of the insertWithTxThrowException()
Propagation.REQUIRES_NEW
to not affect each other:
@Transactional(rollbackFor = Throwable.class)
public void tryCatchRollBackSuccess(String name, String anotherName) {
jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
try {
// 带事务,抛异常回滚
userService.insertWithTxThrowException(anotherName);
} catch (RollBackException e) {
// do nothing
}
}
@Transactional(rollbackFor = Throwable.class, propagation = Propagation.REQUIRES_NEW)
public void insertWithTxThrowException(String name) throws RollBackException {
jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
throw new RollBackException(ROLL_BACK_MESSAGE);
}
PROPAGATION_REQUIRED
and Propagation.REQUIRES_NEW
are sufficient for most application scenarios, which are also commonly used transaction propagation types in development. The former requires that the rollback is based on the same physical transaction, and the latter requires that everyone uses independent transactions without interfering with each other. There is another scenario: the external method and the internal method share a transaction, but the rollback of the internal transaction does not affect the external transaction, and the rollback of the external transaction can affect the internal transaction. This is the usage scenario of nesting this type of propagation.
3)、PROPAGATION_NESTED
Literal meaning: spread-nested
PROPAGATION_NESTED
can set multiple savepoints for rollback on an existing physical transaction. This partial rollback allows internal transactions to be rolled back within its own scope, and at the same time, external transactions can continue to execute after some operations are rolled back. The underlying implementation is savepoint
database.
This propagation mechanism is more flexible than the previous two. Look at the following code:
@Transactional(rollbackFor = Throwable.class)
public void invokeNestedTx(String name,String otherName) {
jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
try {
userService.insertWithTxNested(otherName);
} catch (RollBackException e) {
// do nothing
}
// 如果这里抛出异常,将导致两个方法都回滚
// throw new RollBackException(ROLL_BACK_MESSAGE);
}
@Transactional(rollbackFor = Throwable.class,propagation = Propagation.NESTED)
public void insertWithTxNested(String name) throws RollBackException {
jdbcTemplate.execute("INSERT INTO USER (NAME) VALUES ('" + name + "')");
throw new RollBackException(ROLL_BACK_MESSAGE);
}
The external transaction method invokeNestedTx()
opens the transaction, and the internal transaction method insertWithTxNested
marked as a nested transaction. The rollback of the internal transaction is completed through the save point and will not affect the external transaction. The rollback of the external method will be rolled back together with the internal method.
Summary: This section introduces three common Spring declarative transaction propagation attributes. Combined with the sample code, I believe you will also understand them. Next, let’s take a look at the source code level and see how Spring helps us simplify the transaction template. Code, liberating productivity.
4. Source code snooping
Before reading the source code, first analyze a question: I want to add a transaction to a method, what work do I need to do?
Even if we write by ourselves, at least four steps are required:
- Open transaction
- Execution method
- Roll back the transaction when it encounters an exception
- Commit the transaction after normal execution
Isn't this the typical AOP
~
That's right, Spring enhanced our transaction method through AOP, thus completing transaction-related operations. The source code of several key classes and their key methods are given below.
Since it is AOP, you must write an aspect to the transaction to do this. This class is TransactionAspectSupport
. From the naming, it can be seen that this is the "transaction aspect support class". His main job is to implement the execution process of the transaction, and its main realization The method is invokeWithinTransaction
:
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
// 省略代码...
// Standard transaction demarcation with getTransaction and commit/rollback calls.
// 1、开启事务
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
//2、执行方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
// 3、捕获异常时的处理
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null && txAttr != null) {
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}
//4、执行成功,提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
// 省略代码...
Combining the four-step notes added by the class representative, I believe you will be able to understand it easily.
Understand the main process of the transaction, how is its propagation mechanism realized? This depends AbstractPlatformTransactionManager
the class 06136c0ab24a30. From the name, it can be seen that it is responsible for transaction management. The handleExistingTransaction
method implements the transaction propagation logic. Here is PROPAGATION_REQUIRES_NEW
the following code:
private TransactionStatus handleExistingTransaction(
TransactionDefinition definition, Object transaction, boolean debugEnabled)
throws TransactionException {
// 省略代码...
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
if (debugEnabled) {
logger.debug("Suspending current transaction, creating new transaction with name [" +
definition.getName() + "]");
}
// 事务挂起
SuspendedResourcesHolder suspendedResources = suspend(transaction);
try {
return startTransaction(definition, transaction, debugEnabled, suspendedResources);
}
catch (RuntimeException | Error beginEx) {
resumeAfterBeginException(transaction, suspendedResources, beginEx);
throw beginEx;
}
}
// 省略代码...
}
In the previous article, we know that PROPAGATION_REQUIRES_NEW
will suspend the previous transaction and start an independent new transaction. The database does not support transaction suspension. How does Spring implement this feature?
As you can see from the source code, the suspend(transaction)
SuspendedResourcesHolder
is called, and its actual logic is implemented by the internal abstract method doSuspend(transaction)
Here we are using JDBC
connect to the database. Naturally, we have to choose the DataSourceTransactionManager
to view its implementation. The code is as follows:
protected Object doSuspend(Object transaction) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
txObject.setConnectionHolder(null);
return TransactionSynchronizationManager.unbindResource(obtainDataSource());
}
connection
of the existing transaction and return to the suspended resource. When the transaction is opened next, the suspended resource will be passed in together, so that when the inner transaction is completed, the suspended transaction in the outer layer can be continued.
So, when to continue the execution of the suspended transaction?
Process transactions, although by TransactionAspectSupport
achieved, but really commit, rollback, by AbstractPlatformTransactionManager
to complete, at its processCommit(DefaultTransactionStatus status)
last method finally
block, do the cleanupAfterCompletion(status)
:
private void cleanupAfterCompletion(DefaultTransactionStatus status) {
status.setCompleted();
if (status.isNewSynchronization()) {
TransactionSynchronizationManager.clear();
}
if (status.isNewTransaction()) {
doCleanupAfterCompletion(status.getTransaction());
}
// 有挂起事务则获取挂起的资源,继续执行
if (status.getSuspendedResources() != null) {
if (status.isDebug()) {
logger.debug("Resuming suspended transaction after completion of inner transaction");
}
Object transaction = (status.hasTransaction() ? status.getTransaction() : null);
resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources());
}
}
It is judged that there are suspended resources will resume execution, and the logic of suspending and resuming the transaction is completed.
For the realization of the propagation properties of other affairs, interested students use the sample project represented by the class, and interrupt the point to follow the source code. Due to space limitations, only the approximate processing flow is given here. There are a lot of details in the source code, and students need to experience it by themselves. With the main logic framework introduced above, tracking the source code and viewing other implementations should not be too much trouble.
5. Common failure scenarios
Many people (including the class representative himself) feel that this thing is really a pit when they start using declarative affairs. After using so many rules and regulations, they won't take effect if they are not careful. Why do you feel this way?
After climbing many pits, the class representative summed up two experiences:
- Did not read the official document
- Can't read source code
Here are a few failure scenarios:
1) Non-public methods do not take effect
The official website has instructions:
Method visibility and
@Transactional
When you use transactional proxies with Spring’s standard configuration, you should apply the
@Transactional
annotation only to methods withpublic
visibility.
2) Spring does not support transactions in redis clusters
redis
transaction start command is multi
, but Spring Data Redis does not support the multi command in the redis cluster. If declarative transactions are used, an error will be reported: MULTI is currently not supported in cluster mode.
3) In the case of multiple data sources, you need to configure TransactionManager
for each data source, and specify the transactionManager
parameter
The fourth part of the source code snooping has seen that the actual transaction operation is AbstractPlatformTransactionManager
, which is the implementation class TransactionManager
connection
connection of each transaction is managed by it. If there is no configuration, the transaction operation cannot be completed. The normal operation in the case of a single data source is because SpringBoot's DataSourceTransactionManagerAutoConfiguration
is automatically configured for us.
4) RollbackFor setting error
java.lang.RuntimeException
exceptions (that is, a subclass of java.lang.Error
) and 06136c0ab24cd5 are rolled back by default. If you know that the exception is thrown, you should roll back. It is recommended to set it to @Transactional(rollbackFor = Throwable.class)
5) AOP does not take effect
Others such as MyISAM does not support, es does not support, etc. will not be listed one by one.
If you are interested, all of the above can be found in the source code.
6. Concluding remarks
Regarding Spring's declarative transaction, if you want to use it well, it's really much better to debug the source code several times. Because Spring's source code details are too rich, it is not suitable for posting all of them in the article. I suggest you follow the source code yourself. Once you are familiar with it, you won’t be afraid of encountering failures again.
The following information proves that I am not talking nonsense
1. The test case code in the article: https://github.com/zhengxl5566/springboot-demo/tree/master/transactional
2. Spring official website transaction documents: https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-propagation
3. Oracle official website JDBC documentation: https://docs.oracle.com/javase/tutorial/jdbc/basics/index.html
4. Spring Data Redis source code: https://github.com/spring-projects/spring-data-redis
Past original dry goods
Use Spring Validation to validate parameters
downloaded attachment name is always garbled? You should read the RFC document!
singleton mode, detailed keyword level
Original codewords are not easy, welcome to like, follow and share.
I am in the public : 16136c0ab24dec Java class represents , waiting for you.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。