preface
Spring transactions are certainly familiar to those of you working in Java development.
In some service scenarios, data from multiple tables may be written to a request. Spring transactions are used to ensure atomicity of operations (either simultaneous success or simultaneous failure) and to avoid data inconsistencies.
Sure, Spring transactions are fun to use. They can be Transactional with a simple annotation: @Transactional. I guess most people use it the same way, and they use it all the time.
But it can also trick you if you don’t use it right.
Today we’re going to talk about some of the scenarios where things go wrong, and maybe you’ve already fallen for it. Don’t believe me, let’s take a look.
A transaction does not take effect
1. Access permission problems
As we all know, There are four main types of Java access rights: private, default, protected, and public, which increase from left to right.
However, if we define the wrong access permissions for some transaction methods in the development process, it will lead to problems with the transaction function, such as:
@Service public class UserService { @Transactional private void add(UserModel userModel) { saveData(userModel); updateData(userModel); }}Copy the code
We can see that the add method access is defined as private, which invalidates the transaction, and Spring requires that the propped method be public.
To put it bluntly, a judgment in AbstractFallbackTransactionAttributeSource computeTransactionAttribute method of a class, if the target method is not public, TransactionAttribute returns NULL, indicating that transactions are not supported.
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<? > targetClass) {\ // Don't allow no-public methods as required.\ if (allowPublicMethodsOnly() && ! Modifier.isPublic(method.getModifiers())) {\ return null; \ }\ \ // The method may be on an interface, but we need attributes from the target class.\ // If the target class is null, the method will be unchanged.\ Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); \ \ // First try is the method in the target class.\ TransactionAttribute txAttr = findTransactionAttribute(specificMethod); \ if (txAttr ! = null) {\ return txAttr; \ }\ \ // Second try is the transaction attribute on the target class.\ txAttr = findTransactionAttribute(specificMethod.getDeclaringClass()); \ if (txAttr ! = null && ClassUtils.isUserLevelMethod(method)) {\ return txAttr; \ }\ \ if (specificMethod ! = method) {\ // Fallback is to look at the original method.\ txAttr = findTransactionAttribute(method); \ if (txAttr ! = null) {\ return txAttr; \ }\ // Last fallback is the class of the original method.\ txAttr = findTransactionAttribute(method.getDeclaringClass()); \ if (txAttr ! = null && ClassUtils.isUserLevelMethod(method)) {\ return txAttr; \ }\ }\ return null; The \}Copy the code
That is, spring does not provide transaction functionality if the access to our custom transaction method (the target method) is not public, but private, default, or protected.
2. Methods are final
Sometimes, when a method does not want to be rewritten by subclasses, you can define that method as final. Normal methods are fine, but if transaction methods are defined as final, for example:
@Service public class UserService { @Transactional public final void add(UserModel userModel){ saveData(userModel); updateData(userModel); }}Copy the code
We can see that the Add method is defined as final, which invalidates the transaction.
Why is that?
If you have looked at the source code for Spring transactions, you probably know that aop is used at the bottom of spring transactions. That is, using JDK dynamic proxies or Cglib, it helps us generate proxy classes, which implement transactional functions in proxy classes.
But if a method is decorated with final, it cannot be overridden in its proxy class to add transaction functionality.
Note: If a method is static, it cannot be transacted through a dynamic proxy.
3. Method internal invocation
Sometimes we need to call another transaction method within a method of a Service class, for example:
@Service public class UserService { @Autowired private UserMapper userMapper; @Transactional public void add(UserModel userModel) { userMapper.insertUser(userModel); updateStatus(userModel); } @Transactional public void updateStatus(UserModel userModel) { doSameThing(); }}Copy the code
We see that in the transaction method add, the transaction method updateStatus is called directly. As you can see from the previous section, the updateStatus method has the transactional capability because Spring AOP generates the proxy object, but this method calls the method of this object directly, so the updateStatus method does not generate a transaction.
Thus, a transaction is invalidated when a method in the same class is called directly from within.
So the question is, what if some scenario really wants to call another method of its own within a method of the same class?
3.1 Add a Service method
This method is as simple as adding a new Service method, adding the @Transactional annotation to the new Service method, and moving the Transactional code to the new method. The specific code is as follows:
@Servcie public class ServiceA { @Autowired prvate ServiceB serviceB; public void save(User user) { queryData1(); queryData2(); serviceB.doSave(user); } } @Servcie public class ServiceB { @Transactional(rollbackFor=Exception.class) public void doSave(User user) { addData1(); updateData2(); }}Copy the code
3.2 Inject itself into the Service class
If you don’t want to add a new Service class, injecting yourself into the Service class is an option. The specific code is as follows:
@Servcie public class ServiceA { @Autowired prvate ServiceA serviceA; public void save(User user) { queryData1(); queryData2(); serviceA.doSave(user); } @Transactional(rollbackFor=Exception.class) public void doSave(User user) { addData1(); updateData2(); }}Copy the code
Some people might wonder: Does this approach create a cyclic dependency problem? Answer: No.
In fact, spring IOC’s internal level 3 cache ensures that it does not have cyclic dependencies. However, if you want to learn more about circular dependencies, check out my previous article “Spring: How Do I Solve circular dependencies?” .
3.3 Through the AopContent class
Use aopContext.currentProxy () in the Service class to get the proxy object
Method 2 above does solve the problem, but the code doesn’t look intuitive. You can do the same thing by using AOPProxy in the Service class to get the proxy object. The specific code is as follows:
@Servcie public class ServiceA { public void save(User user) { queryData1(); queryData2(); ((ServiceA)AopContext.currentProxy()).doSave(user); } @Transactional(rollbackFor=Exception.class) public void doSave(User user) { addData1(); updateData2(); }}Copy the code
4. Not managed by Spring
There is one detail that is easily overlooked in our development process. The premise of using Spring transactions is that for objects to be managed by Spring, you need to create bean instances.
In general, bean instantiation and dependency injection can be implemented automatically with annotations @Controller, @Service, @Component, @repository, etc.
Of course, there are many other ways to create bean instances. If you are interested, you can check out another article I wrote earlier, “@autowired: Do you know all these crazy operations?”
If one day you were in a hurry to develop a Service class and forgot to add the @service annotation, for example:
//@Service public class UserService { @Transactional public void add(UserModel userModel) { saveData(userModel); updateData(userModel); }}Copy the code
From the above example, we can see that the UserService class is not annotated with @service, so the class is not managed by Spring, so its Add method does not generate transactions.
5. Multi-threaded calls
In actual project development, the use of multithreading is quite a lot. Is there a problem if Spring transactions are used in multi-threaded scenarios?
@Slf4j @Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { userMapper.insertUser(userModel); new Thread(() -> { roleService.doOtherThing(); }).start(); }} @transactional public class RoleService {@transactional public void doOtherThing() {system.out.println (); }}Copy the code
From the above example, we can see that the transaction method doOtherThing is called in the transaction method add, but the transaction method doOtherThing is called in a different thread.
This will result in two methods not in the same thread, obtaining different database connections, and thus two different transactions. It is not possible to roll back the add method if an exception is thrown in the doOtherThing method.
Those of you who have read the source code for Spring transactions probably know that spring transactions are implemented through database connections. The current thread holds a map where key is the data source and value is the database connection.
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
Copy the code
When we say the same transaction, we actually mean the same database connection, only the same database connection can be committed and rolled back. If you are in a different thread, the database connection must be different, so it is a different transaction.
6. Tables do not support transactions
As you know, before mysql5, the default database engine was MyISam.
The benefits are obvious: index files and data files are stored separately, providing better performance than InnoDB for single-table operations with more searches and fewer writes.
You might still be using it in some of your older projects.
When creating a table, simply set the ENGINE parameter to MyISAM:
CREATE TABLE `category` (
`id` bigint NOT NULL AUTO_INCREMENT,
`one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
Copy the code
Myisam works, but it has a fatal problem: it doesn’t support transactions.
If only a single table operation is fine, there will not be too much of a problem. However, if you need to operate across multiple tables, the data will most likely be incomplete because it does not support transactions.
In addition, MyISam does not support row locking and foreign keys.
So in real business scenarios, MyISAM is not used much. After mysql5, MyISam has been phased out and replaced by InnoDB.
Sometimes during development, we find that a table transaction never takes effect. It is not necessarily the spring transaction pot. It is best to make sure that the table you are using supports transactions.
7. The transaction is not started
Sometimes, the root cause of a transaction not taking effect is that the transaction was not started.
You might laugh at this.
Isn’t opening transactions one of the most basic functions of a project?
Why is the transaction still not opened?
Yes, if the project is already built, there will be transaction functionality.
But if you’re building a demo and there’s only one table, the transaction for that table doesn’t work. So what could be causing this?
There are many reasons, of course, but the reason for not starting a transaction is extremely easy to overlook.
If you’re using the Springboot program, you’re in luck. Because springboot through DataSourceTransactionManagerAutoConfiguration class, already silently help you open the transaction.
All you need to do is configure the spring.datasource parameters.
However, if you are using a traditional Spring project, you will need to manually configure transaction parameters in the applicationContext.xml file. If you forget to configure, the transaction will definitely not take effect.
The configuration information is as follows:
<! - configuration transaction manager - > < bean class = ". Org. Springframework. JDBC datasource. DataSourceTransactionManager "id =" transactionManager "> <property name="dataSource" ref="dataSource"></property> </bean> <tx:advice id="advice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="*" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <! < AOP :config> < AOP :pointcut expression="execution(* com.susan.*.*(..)) )" id="pointcut"/> <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/> </aop:config>Copy the code
Silently, if pointcuts in the pointcut tag match the rules incorrectly, some classes of transactions will not take effect.
Transaction does not roll back
1. False propagation characteristics
The Propagation parameter can be specified when using the @Transactional annotation.
This parameter specifies the propagation characteristics of a transaction. Spring currently supports seven propagation characteristics:
REQUIRED
If a transaction exists in the current context, join it, and if none exists, create one, which is the default propagation property value.SUPPORTS
If a transaction exists in the current context, the transaction is supported to join the transaction; if not, the transaction is executed in a non-transactional manner.MANDATORY
If a transaction exists in the current context, otherwise an exception is thrown.REQUIRES_NEW
Each time, a new transaction is created, and the transaction in the context is suspended at the same time. When the newly created transaction is completed, the context transaction is resumed for execution.NOT_SUPPORTED
If a transaction exists in the current context, the current transaction is suspended, and the new method executes in a transaction-free environment.NEVER
Throw an exception if a transaction exists in the current context, otherwise execute the code on a transaction-free environment.NESTED
If a transaction exists in the current context, the nested transaction executes, if none exists, a new transaction is created.
If the propagation parameters are set incorrectly, for example:
@Service
public class UserService {
@Transactional(propagation = Propagation.NEVER)
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
Copy the code
Propagation.NEVER. This type of Propagation does not support transactions and will throw an exception if there are transactions.
New transactions are currently created only with these three propagation features: REQUIRED, REQUIRES_NEW, and NESTED.
2. He swallowed an abnormality
Transactions do not roll back. The most common problem is that developers manually try… An exception was caught. Such as:
@Slf4j @Service public class UserService { @Transactional public void add(UserModel userModel) { try { saveData(userModel); updateData(userModel); } catch (Exception e) { log.error(e.getMessage(), e); }}}Copy the code
In this case, of course, the Spring transaction does not roll back, because the developer caught the exception and did not throw it manually, in other words, it swallowed the exception.
If you want a Spring transaction to roll back properly, you must throw an exception that it can handle. If no exceptions are thrown, Spring considers the program to be healthy.
3. Other exceptions are manually thrown
Even if the developer does not catch the exception manually, the Spring transaction does not roll back if the exception is thrown incorrectly.
@Slf4j @Service public class UserService { @Transactional public void add(UserModel userModel) throws Exception { try { saveData(userModel); updateData(userModel); } catch (Exception e) { log.error(e.getMessage(), e); throw new Exception(e); }}}Copy the code
In the above case, the developer catches the Exception and throws it manually: Exception, and the transaction is also not rolled back.
Because of Spring transactions, only RuntimeExceptions and errors are rolled back by default, and plain exceptions (non-runtime exceptions) are not rolled back.
4. The rollback exception is customized
When declaring transactions using the @Transactional annotation, sometimes we want to customize the exceptions that are rolled back. Spring also supports this. You can do this by setting the rollbackFor parameter.
If this parameter is set to the wrong value, it can lead to some puzzling questions, such as:
@Slf4j @Service public class UserService { @Transactional(rollbackFor = BusinessException.class) public void add(UserModel userModel) throws Exception { saveData(userModel); updateData(userModel); }}Copy the code
SqlException; DuplicateKeyException; SqlException; DuplicateKeyException BusinessException is our custom exception. The exception reported is not BusinessException, so the transaction will not be rolled back.
Even though rollbackFor has a default value, the Alibaba developer specification requires developers to re-specify this parameter.
Why is that?
If you use the default values, the transaction will not be rolled back once an Exception is thrown, which can be very buggy. Therefore, you are advised to set this parameter to Exception or Throwable.
5. Nested transactions roll back a lot
public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { userMapper.insertUser(userModel); roleService.doOtherThing(); } } @Service public class RoleService { @Transactional(propagation = Propagation.NESTED) public void doOtherThing() { System.out.println(" Save role table data "); }}Copy the code
This case uses a nested internal transaction, and the original intention is to roll back only the contents of the doOtherThing method and not the contents of the usermapper.insertUser savepoint if an exception occurs when the roleService. DoOtherThing method is called. But the truth is, insertUser also rolls back.
why?
Because the doOtherThing method had an exception, no manual catch was made and the exception was caught in the proxy method of the outer Add method. So, in this case, the entire transaction is rolled back directly, not just a single savepoint.
How can I just roll back savepoints?
@Slf4j @Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { userMapper.insertUser(userModel); try { roleService.doOtherThing(); } catch (Exception e) { log.error(e.getMessage(), e); }}}Copy the code
You can place an internally nested transaction in a try/catch and not continue throwing exceptions. This ensures that if an exception occurs in an internally nested transaction, only the internal transaction is rolled back and the external transaction is not affected.
Three other
1 big business issues
One of the biggest headaches when using Spring transactions is the big transaction problem.
Usually we annotate methods with the @Transactional annotation, such as:
@Service public class UserService { @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { query1(); query2(); query3(); roleService.save(userModel); update(userModel); } } @Service public class RoleService { @Autowired private RoleService roleService; @Transactional public void save(UserModel userModel) throws Exception { query4(); query5(); query6(); saveData(userModel); }}Copy the code
The downside of the @Transactional annotation, when applied to methods, is that the whole method is included in the transaction.
In the example above, in the UserService class, these are actually the only two lines that require a transaction:
roleService.save(userModel);
update(userModel);
Copy the code
In the RoleService class, this is the only line that requires a transaction:
saveData(userModel);
Copy the code
The current writing results in all query methods being included in the same transaction.
If the query method is very many, the call level is very deep, and some of the query methods are time-consuming, the whole transaction can be very time-consuming, which can cause large transaction problems.
For more on the dangers of big Business problems, read my article, “How can Big Business Headaches Be Solved?” There are detailed explanations above.
2. Programmatic transactions
Talk these contents are based on the above @ Transactional annotation, main said is its transaction problem, we call this transaction: declarative transaction.
In fact, Spring provides another way to create transactions, namely, transactions that are implemented through manual code, which we call programmatic transactions. Such as:
@Autowired private TransactionTemplate transactionTemplate; . public void save(final User user) { queryData1(); queryData2(); transactionTemplate.execute((status) => { addData1(); updateData2(); return Boolean.TRUE; })}Copy the code
Spring provides a class to support programmatic transactions, TransactionTemplate, which implements transactions in its execute method.
Instead of @Transactional annotation declarative transactions, I recommend programmatic transactions based on TransactionTemplate. The main reasons are as follows:
- Avoid transaction failures due to Spring AOP issues.
- The ability to control the scope of transactions at a smaller granularity is more intuitive.
It is recommended that you use the @Transactional annotation sparingly in your projects to open transactions. That’s not to say you shouldn’t use it. If you have simple business logic in your project that doesn’t change often, it’s okay to use the @Transactional annotation to start transactions. It’s simpler and more efficient to develop, but be careful about transaction failures.