table of Contents
Preface
Affairs failed
database level
business code level
summary
transaction rollback issues related to
problems with using separate read and write transactions combined
summary
Preface
In the previous article, we have done a detailed analysis of the transaction in Spring. In this article, let’s talk about some problems that may arise when using transactions in ordinary work (this article mainly @Transactional
discusses the use of transaction management) and Corresponding solution
- Transaction failure
- Issues related to transaction rollback
- Problems when read-write separation is used in combination with transactions
Transaction failure
We generally need to troubleshoot issues from two aspects
Database level
At the database level, does the storage engine used by the database support transactions? By default, the MySQL database uses the Innodb storage engine (after version 5.5), which supports transactions, but if your table specifically modifies the storage engine, for example, you modify the storage engine used by the table through the following statement MyISAM
: and MyISAM
it is not transactional
alter table table_name engine=myisam;
This will cause the "transaction failure" problem
Solution : Modify the storage engine as Innodb
.
Business code level
Whether there is a problem with the business-level code, there are many possibilities
- We want to use Spring's declarative transaction, so has the bean that needs to execute the transaction been managed by Spring? The embodiment in the code is whether there is a series of annotations on the class
@Service
,Component
etc.
**Solution:** Hand over the Bean to Spring for management (add @Service
note)
@Transactional
Whether the annotation is placed in the proper place. In the last article, we did a detailed analysis of the principle of transaction failure in Spring, which also analyzed how Spring internally parses@Transactional
annotations. Let's review the code a bit:
Code is located:
AbstractFallbackTransactionAttributeSource#computeTransactionAttribute
in
In other words, you cannot use @Transactional
transaction management on a non-public method by default
** Solution: ** Modify the method that requires transaction management public
.
- Self-calling appeared. What is self-calling? Let's look at an example
@Service
public class DmzService {
public void saveAB(A a, B b) {
saveA(a);
saveB(b);
}
@Transactional
public void saveA(A a) {
dao.saveA(a);
}
@Transactional
public void saveB(B b){
dao.saveB(a);
}
}
The above three methods are in the same class DmzService
, where the saveAB
method is called in this class saveA
with the saveB
method, which is self call. In the example above saveA
with saveB
the transaction will fail on
So why does self-invoking cause transaction failure? We know that the implementation of transactions in Spring is dependent AOP
. When the container creates dmzService
this Bean, it finds that there is an @Transactional
annotated method in this class (modifier is public), then we need to create a proxy object for this class and put it in In the container, the proxy object created is equivalent to the following class
public class DmzServiceProxy {
private DmzService dmzService;
public DmzServiceProxy(DmzService dmzService) {
this.dmzService = dmzService;
}
public void saveAB(A a, B b) {
dmzService.saveAB(a, b);
}
public void saveA(A a) {
try {
// 开启事务
startTransaction();
dmzService.saveA(a);
} catch (Exception e) {
// 出现异常回滚事务
rollbackTransaction();
}
// 提交事务
commitTransaction();
}
public void saveB(B b) {
try {
// 开启事务
startTransaction();
dmzService.saveB(b);
} catch (Exception e) {
// 出现异常回滚事务
rollbackTransaction();
}
// 提交事务
commitTransaction();
}
}
The above is a piece of pseudo-code, through startTransaction
, rollbackTransaction
, commitTransaction
the logical agent SIMULATION three classes. Because there are annotations on DmzService
the saveA
follow saveB
method in the target class , @Transactional
these two methods will be intercepted and the transaction management logic will be embedded. At the same time saveAB
, there is no method @Transactional
, which is equivalent to the proxy class directly calling the method in the target class.
We will find that saveAB
the call chain of the entire method when called through the proxy class is as follows:
In fact, we call saveA
now saveB
called when a target is a class, this empty, of course, the transaction will fail.
Another example of common transaction failure caused by self-call is as follows:
@Service
public class DmzService {
@Transactional
public void save(A a, B b) {
saveB(b);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveB(B b){
dao.saveB(a);
}
}
When we call the save
method, our expected execution flow is like this
In other words, the two transactions do not interfere with each other, and each transaction has its own open, rollback, and commit operations.
But according to the previous analysis, we know that when the saveB method is called, the saveB method in the target class is directly called. There will be no transaction opening or committing or rolling back before and after the saveB method. The actual process is The following
Since the saveB method is actually called by dmzService, that is, the target class itself, transaction-related operations are not performed before and after the saveB method. This is also the root cause of the problem caused by self-calling : when self-calling, the method in the target class is called instead of the method in the proxy class
Solution :
-
Inject yourself and then display the call, for example:
@Service public class DmzService { // 自己注入自己 @Autowired DmzService dmzService; @Transactional public void save(A a, B b) { dmzService.saveB(b); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveB(B b){ dao.saveB(a); } }
This scheme does not look very elegant
-
Use
AopContext
as follows:@Service public class DmzService { @Transactional public void save(A a, B b) { ((DmzService) AopContext.currentProxy()).saveB(b); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveB(B b){ dao.saveB(a); } }
Use the above solution to note that you need to add a configuration to the configuration class
// exposeProxy=true代表将代理类放入到线程上下文中,默认是false @EnableAspectJAutoProxy(exposeProxy = true)
Personally prefer the second way
Here we make a summary
to sum up
A picture is worth a thousand words
Issues related to transaction rollback
Issues related to rollback can be summarized in two sentences
- The transaction was committed when I wanted to roll back
- When you want to submit, it is marked as rollback only (rollback only)
Let's look at the first situation: the transaction is committed when you want to roll back . This situation is often caused by the programmer's rollbackFor
inadequate understanding of the properties of transactions in Spring .
Spring unchecked by default throws
unchecked
an exception (inherited fromRuntimeException
exception) orError
only roll back the transaction; other exception does not trigger roll back the transaction, SQL has been executed submitted out. If you throw other types of exceptions in the transaction, but expect Spring to roll back the transaction, you need to specify therollbackFor
properties.
The corresponding code was actually analyzed in the previous article, as follows:
The above code is located in:
TransactionAspectSupport#completeTransactionAfterThrowing
method
By default, only appear RuntimeException
or Error
roll back
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
So, if you want to roll back RuntimeException
even Error
when there is a non- or , please specify the exception during rollback, for example:
@Transactional(rollbackFor = Exception.class)
The second case: when you want to submit it is marked as rollback only (rollback only) .
The corresponding exception information is as follows:
Transaction rolled back because it has been marked as rollback-only
Let's look at an example first
@Service
public class DmzService {
@Autowired
IndexService indexService;
@Transactional
public void testRollbackOnly() {
try {
indexService.a();
} catch (ClassNotFoundException e) {
System.out.println("catch");
}
}
}
@Service
public class IndexService {
@Transactional(rollbackFor = Exception.class)
public void a() throws ClassNotFoundException{
// ......
throw new ClassNotFoundException();
}
}
In the above example, DmzService
the testRollbackOnly
method and IndexService
the a
method both open the transaction, and the propagation level of the transaction is required
, so when we testRollbackOnly
call IndexService
the a
method in the method, these two methods should be a common transaction. According to this kind of thinking, although IndexService
the a
method throws an exception, but we are testRollbackOnly
catching the exception, then the transaction should be submitted normally. Why is the exception thrown?
If you have read my previous source code analysis article, you should know that there is such a piece of code when dealing with rollback
The following judgment was made at the time of submission ( I deleted some unimportant codes for this method )
It can be seen that when it is submitted that the transaction has been marked as rollbackOnly, it will enter the rollback process, and the unexpected passed in is true. When processing the rollback, there is the following code
Finally this exception was thrown here.
The above codes are located
AbstractPlatformTransactionManager
in
To sum up, the main reason is that the entire large transaction is marked as rollbackOnly when the internal transaction is rolled back , so even if we catch the thrown exception in the external transaction, the entire transaction still cannot be submitted normally, and if you want it to be normal Submit, Spring will also throw an exception.
Solution :
The solution depends on the business, you have to be clear about what the result you want is
- Abnormalities occur in internal affairs, and after external affairs catch exceptions, internal affairs will automatically roll back without affecting external affairs
The propagation level of internal affairs can be set to nested/requires_new. In our example, the following modifications are made:
// @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW) @Transactional(rollbackFor = Exception.class,propagation = Propagation.NESTED) public void a() throws ClassNotFoundException{ // ...... throw new ClassNotFoundException(); }
Although both can get the above results, there are still differences between them. When the propagation level requires_new
, the two transactions are totally unrelated, each has its own transaction management mechanisms (open transaction, the transaction is closed, roll back the transaction). But the spread level nested
just set a save point when you call a method, in fact, there is only one transaction, when a rollback method actually roll back to the save point, and when the external transaction commits, internal The transaction will be committed, and if the external transaction is rolled back, the internal transaction will be rolled back.
- When an internal transaction is abnormal, after the external transaction catches the exception, both internal and external transactions are rolled back, but the method does not throw an exception
@Transactional public void testRollbackOnly() { try { indexService.a(); } catch (ClassNotFoundException e) { // 加上这句代码 TransactionInterceptor.currentTransactionStatus().setRollbackOnly(); } }
The status of the transaction is displayed through the settings RollbackOnly
. So when the transaction is committed, the following code will be entered
The biggest difference is that false is passed in the second parameter when processing the rollback, which means that the rollback is expected, so no exception will be thrown after the rollback is processed.
Problems when read-write separation is used in combination with transactions
There are generally two ways to achieve read-write separation
- Configure multiple data sources
- Rely on middleware, such as
MyCat
If multiple data sources are configured to achieve read-write separation, you need to pay attention to: if a read-write transaction is enabled, then a write node must be used , if it is a read-only transaction, then a read node can be used
If you rely on MyCat
middleware, you need to pay attention: as long as the transaction is enabled, the SQL in the transaction will use the write node (depending on the implementation of the specific middleware, it may also be allowed to use the read node, the specific strategy needs to be confirmed with the DB team)
Based on the above conclusions, we should be more cautious when using transactions, and try not to open them when there is no need to open them.
Generally, we will configure certain agreed method name prefixes in the configuration file to enable different transactions (or not to enable them), but now with the popularity of annotation transactions, many developers (or architects) add to the service class when building a framework On the @Transactional annotation, the entire class is opened for transactions, which seriously affects the efficiency of database execution. More importantly, developers don’t pay attention to it or don’t know how to add @Transactional(propagation=Propagation .NOT_SUPPORTED) will result in that all the query methods do not actually go to the slave library, resulting in excessive pressure on the master library.
Secondly, if there is no optimization for read-only transactions (optimization means routing read-only transactions to read nodes), then @Transactional
the readOnly
attributes in the annotations should be used with caution. readOnly
The original purpose of our use is to mark the transaction as read-only, so that when the MySQL server detects that it is a read-only transaction, it can optimize and allocate less resources (for example: read-only transactions do not need to be rolled back, so there is no need Allocate undo log segment). However, when read-write separation is configured, it may cause all SQL in the read-only transaction to be routed to the main database, and read-write separation loses its meaning.