preface

The Spring framework has become a standard part of JAVA projects, and Spring transaction management is one of the most commonly used features, but if you don’t understand how it works and use it in the wrong way, you can fall into a trap.

In order to more thoroughly explain these pits, this article is divided into four parts: The first part briefly introduces several ways of Spring transaction integration; The second part explains the implementation principle of Spring transaction based on Spring source code. The third part introduces the pit of Spring transactions through actual test code; The fourth part is the summary of this paper.

Spring transaction Management:

Spring transactions fall into two broad categories in terms of how they are used:

1. The declarative

Based on the TransactionProxyFactoryBean declarative transaction management

And namespace-based transaction management

Transactional Transactional Transactional management

2. Programmatic

Programmatic transaction management based on transaction manager API

Programmatic transaction management based on TransactionTemplate

Most current projects use the latter two declarative ones:

Declarative transaction management based on and namespace can make full use of the powerful support of pointcut expression, making the management of transactions more flexible. The @Transactional approach requires transaction management to be implemented using @Transactional annotations on methods or classes that specify transaction rules. This annotation is often recommended for marking transactions in Spring Boot.

Second, Spring transaction implementation mechanism

Let’s take a closer look at the source code for Spring transactions to see how it works. Let’s start with the parse class for the tag:

@Override public void init() { registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser()); registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser()); registerBeanDefinitionParser("jta-transaction-manager", new JtaTransactionManagerBeanDefinitionParser()); }}Copy the code
class TxAdviceBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
    @Override
    protected Class<?> getBeanClass(Element element) {
        return TransactionInterceptor.class;
    }
}

Copy the code

The core Spring transaction implementation class TransactionInterceptor and its parent class TransactionAspectSupport implement transaction start, database operation, transaction commit, rollback, etc. We can also debug breakpoints in this method if we want to determine whether we are in a transaction during development.

We have compiled the 2021 Java Engineer classic interview questions, a total of 485 pages of approximately 850 interview questions with answers. Java, MyBatis, ZooKeeper, Dubbo, Elasticsearch, Memcached, Redis, MySQL, Spring, Spring Boot, Spring Cloud, RabbitMQ, Kafka, Linux And so on almost all technology stack, each technology stack has not less than 50 classic interview real questions, dare not say to brush the package you into the big factory, but targeted brush let you face the interviewer more than a few minutes of confidence or no problem.

TransactionInterceptor:

public Object invoke(final MethodInvocation invocation) throws Throwable { Class<? > targetClass = (invocation.getThis() ! = null ? AopUtils.getTargetClass(invocation.getThis()) : null); // Adapt to TransactionAspectSupport's invokeWithinTransaction... return invokeWithinTransaction(invocation.getMethod(), targetClass, new InvocationCallback() { @Override public Object proceedWithInvocation() throws Throwable { return invocation.proceed(); }}); }Copy the code

TransactionAspectSupport

protected Object invokeWithinTransaction(Method method, Class<? > targetClass, final InvocationCallback invocation) throws Throwable { // If the transaction attribute is null, the method is non-transactional. final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass); final PlatformTransactionManager tm = determineTransactionManager(txAttr); final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); if (txAttr == null || ! (tm instanceof CallbackPreferringPlatformTransactionManager)) { // Standard transaction demarcation with getTransaction and commit/rollback calls. TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal = null; try { // This is an around advice: Invoke the next interceptor in the chain. // This will normally result in a target object being invoked. retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { // target invocation exception completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); } commitTransactionAfterReturning(txInfo); return retVal; }}Copy the code

So far we have seen the entire invocation flow of a transaction, but there is an important mechanism that has not been analyzed, which is that Spring transactions control the currently acquired database connection for different propagation levels. Spring’s DataSourceUtils tool class is used to fetch connections. JdbcTemplate and Mybatis-Spring also use this class to fetch connections.

Public abstract class DataSourceUtils {... public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { try { return doGetConnection(dataSource); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex); } } public static Connection doGetConnection(DataSource dataSource) throws SQLException { Assert.notNull(dataSource, "No DataSource specified"); ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); if (conHolder ! = null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { conHolder.requested(); if (! conHolder.hasConnection()) { logger.debug("Fetching resumed JDBC Connection from DataSource"); conHolder.setConnection(dataSource.getConnection()); } return conHolder.getConnection(); }... }Copy the code

TransactionSynchronizationManager is the core of a transaction synchronization management class, it implements the transaction synchronization management functions, including records the current connection hold connection holder.

TransactionSynchronizationManager

private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<Map<Object, Object>>("Transactional resources"); ... public static Object getResource(Object key) { Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); Object value = doGetResource(actualKey); if (value ! = null && logger.isTraceEnabled()) { logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]"); } return value; } /** * Actually check the value of the resource that is bound for the given key. */ private static Object doGetResource(Object actualKey) { Map<Object, Object> map = resources.get(); if (map == null) { return null; } Object value = map.get(actualKey); // Transparently remove ResourceHolder that was marked as void... if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) { map.remove(actualKey); // Remove entire ThreadLocal if empty... if (map.isEmpty()) { resources.remove(); } value = null; } return value; }Copy the code

In the transaction manager class AbstractPlatformTransactionManager getTransaction acquisition transaction, will handle the transaction propagation behavior of different, such as the current affairs, However, if the transaction propagation level of the calling method is REQUIRES_NEW or PROPAGATION_NOT_SUPPORTED, operations such as suspension and recovery are performed on the current transaction to ensure that the current database operation obtains the correct Connection.

Concrete is at the end of the sub transaction commit will suspend transaction recovery, recovery to call TransactionSynchronizationManager. BindResource set before the connection of the holder, so to get the connection is restored the database connection, TransactionSynchronizationManager active connection can only be one.

AbstractPlatformTransactionManager

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 { boolean newSynchronization = (getTransactionSynchronization() ! = SYNCHRONIZATION_NEVER); DefaultTransactionStatus status = newTransactionStatus( definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); doBegin(transaction, definition); prepareSynchronization(status, definition); return status; } catch (RuntimeException beginEx) { resumeAfterBeginException(transaction, suspendedResources, beginEx); throw beginEx; } catch (Error beginErr) { resumeAfterBeginException(transaction, suspendedResources, beginErr); throw beginErr; } } /** * Clean up after completion, clearing synchronization if necessary, * and invoking doCleanupAfterCompletion. * @param status object representing the transaction * @see #doCleanupAfterCompletion */ 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"); } resume(status.getTransaction(), (SuspendedResourcesHolder) status.getSuspendedResources()); }}Copy the code

Spring transactions are implemented through an Advice (TransactionInterceptor) in the AOP proxy class, and the propagation level defines how transactions and subtransactions acquire connections, commit transactions, and roll back.

AOP (Aspect Oriented Programming), namely, section-oriented Programming. Spring AOP technology implementation is actually the proxy class, specifically can be divided into static proxy and dynamic proxy two categories, static proxy refers to the use of THE COMMAND provided by the AOP framework for compilation, so that in the compilation stage can generate AOP proxy class, so also called compile-time enhancement; (AspectJ); Dynamic proxies, on the other hand, generate AOP dynamic proxy classes “temporarily” in memory at run time with the help of a dummy class library, and are therefore also known as runtime enhancement. Java is the dynamic proxy mode used (JDK+CGLIB).

JDK dynamic proxies JDK dynamic proxies mainly involve two classes in the java.lang.Reflect package: Proxy and InvocationHandler. InvocationHandler is an interface that dynamically marshals crosscutting logic and business logic by implementing the interface that defines crosscutting logic and invokes the code of the target class through reflection. Proxy uses InvocationHandler to dynamically create an instance that conforms to an interface and generate a Proxy object for the target class.

CGLIB dynamic proxy CGLIB full name Code Generation Library, is a powerful high-performance, high-quality Code Generation class Library, you can extend Java classes and implement Java interface at runtime, CGLIB encapsulation ASM, you can dynamically generate new classes at runtime. Compare this to JDK dynamic proxies: JDK creation of proxies is limited to creating proxy instances for interfaces, whereas dynamic proxies can be created using CGLIB for classes that do not define business methods through interfaces.

CGLIB is slow to create proxies, but very fast to run once they are created, whereas JDK dynamic proxies are the opposite. If CGLIB is constantly used to create agents at run time, the performance of the system will suffer. So if there is an interface, Spring uses JDK dynamic proxies by default. The source code is as follows:

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { @Override public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class<? > targetClass = config.getTargetClass(); if (targetClass == null) { throw new AopConfigException("TargetSource cannot determine target class: " + "Either an interface or a target is required for proxy creation."); } if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); } return new ObjenesisCGLIBAopProxy(config); } else { return new JdkDynamicAopProxy(config); }}}Copy the code

Now that we have learned about the two characteristics of the Spring proxy, we know some considerations when configuring the transaction aspect, such as JDK proxy methods must be public, CGLIB proxy methods must be public, protected, and classes must not be final. During dependency injection, if the property type is defined as an implementation class, the JDK agent will report the following injection exception:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.wwb.test.TxTestAop': Unsatisfied dependency expressed through field 'service'; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'stockService' is expected to be of type 'com.wwb.service.StockProcessServiceImpl' but was actually of type 'com.sun.proxy.$Proxy14'

Copy the code

However, if the CGLIB proxy is changed to the CGLIB proxy, it will be successfully injected. Therefore, if there is an interface, you are advised to define the attributes of this class as interfaces during the injection. In addition, transaction pointcuts can be configured on both the implementation class and interface, but it is recommended to add them to the implementation class.

The official website provides a detailed introduction to Spring AOP

Docs. Spring. IO/spring/docs…

Third, the pit of Spring transactions

From the previous chapters, you have a good grasp of how spring transactions work and how they work, but be careful because you can get tripped up if you’re not careful. First look at the first pit:

3.1 Transaction does not take effect

Test code, transactional AOP configuration:

<tx:advice id="txAdvice" transaction-manager="myTxManager"> <tx:attributes> <! <tx:method name="openAccount" Isolation ="DEFAULT" Propagation ="REQUIRED"/> <tx:method name="openStock" isolation="DEFAULT" propagation="REQUIRED"/> <tx:method name="openStockInAnotherDb" isolation="DEFAULT"  propagation="REQUIRES_NEW"/> <tx:method name="openTx" isolation="DEFAULT" propagation="REQUIRED"/> <tx:method name="openWithoutTx" isolation="DEFAULT" propagation="NEVER"/> <tx:method name="openWithMultiTx" isolation="DEFAULT" propagation="REQUIRED"/> </tx:advice>Copy the code
public class StockProcessServiceImpl implements IStockProcessService{ @Autowired private IAccountDao accountDao; @Autowired private IStockDao stockDao; @Override public void openAccount(String aname, double money) { accountDao.insertAccount(aname, money); } @Override public void openStock(String sname, int amount) { stockDao.insertStock(sname, amount); } @Override public void openStockInAnotherDb(String sname, int amount) { stockDao.insertStock(sname, amount); } } public void insertAccount(String aname, double money) { String sql = "insert into account(aname, balance) values(? ,?) "; this.getJdbcTemplate().update(sql, aname, money); DbUtils.printDBConnectionInfo("insertAccount",getDataSource()); } public void insertStock(String sname, int amount) { String sql = "insert into stock(sname, count) values (? ,?) "; this.getJdbcTemplate().update(sql , sname, amount); DbUtils.printDBConnectionInfo("insertStock",getDataSource()); } public static void printDBConnectionInfo(String methodName,DataSource ds) { Connection connection = DataSourceUtils.getConnection(ds); System.out.println(methodName+" connection hashcode="+connection.hashCode()); }Copy the code
Public void openTx(String aname, double money) {openAccount(aname,money); openStock(aname,11); }Copy the code

1. Running output:

insertAccount connection hashcode=319558327 insertStock connection hashcode=319558327

Public void openWithoutTx(String aname, double money) {openAccount(aname,money); openStock(aname,11); }Copy the code

2. Running output:

insertAccount connection hashcode=1333810223 insertStock connection hashcode=1623009085

@override public void openWithMultiTx(String aname, double money) { openAccount(aname,money); openStockInAnotherDb(aname, 11); // Propagation level for REQUIRES_NEW}Copy the code

3. Running output:

insertAccount connection hashcode=303240439 insertStock connection hashcode=303240439

We can see that the 2 and 3 test methods are not as expected by our transaction. Conclusion: The calling method does not configure the transaction, and the class method is directly called, the transaction will not take effect!

This is because Spring transactions are essentially proxy classes, and when methods of this class are called directly, the object itself is not a proxy woven into the transaction, so the transaction aspect does not take effect. See the #Spring Transaction Implementation mechanism section for details.

Spring also provides a way to determine whether it is a proxy:

public static void printProxyInfo(Object bean) {
        System.out.println("isAopProxy"+AopUtils.isAopProxy(bean));
        System.out.println("isCGLIBProxy="+AopUtils.isCGLIBProxy(bean));
        System.out.println("isJdkProxy="+AopUtils.isJdkDynamicProxy(bean));
    }

Copy the code

So how do you change that to a proxy class call? The most straightforward idea is to inject itself, as follows:

@Autowired private IStockProcessService stockProcessService; / / into the class itself, circular dependencies, kiss can measure public void openTx (String aname, double money) {stockProcessService. OpenAccount (aname, money); stockProcessService.openStockInAnotherDb (aname,11); }Copy the code

Of course, Spring provides a way to get the current proxy:

@override public void openWithMultiTx(String aname, double money) { ((IStockProcessService)AopContext.currentProxy()).openAccount(aname,money); ((IStockProcessService)AopContext.currentProxy()).openStockInAnotherDb(aname, 11); }Copy the code

Another Spring is through TransactionSynchronizationManager thread class variables to obtain in the transaction database connection, so if it is multi-thread calls or bypass the Spring for the database connection, will cause the Spring transaction configuration failure.

Final Spring transaction configuration failure scenario:

The transaction aspect is not configured correctly

This class method is called

Multithreaded call

Bypass Spring to get the database connection

Let’s look at another pitfall in Spring transactions:

3.2 Transactions are not rolled back

Test code:

<tx:advice id="txAdvice" transaction-manager="myTxManager"> <tx:attributes> <! <tx:method name="buyStock" Isolation ="DEFAULT" Propagation ="REQUIRED"/> </tx: Attributes > </tx:advice>Copy the code
public void buyStock(String aname, double money, String sname, int amount) throws StockException { boolean isBuy = true; accountDao.updateAccount(aname, money, isBuy); If (true) {throw new StockException(" buy StockException "); } stockDao.updateStock(sname, amount, isBuy); }Copy the code
 @Test
    public void testBuyStock() {
        try {
            service.openAccount("dcbs", 10000);
            service.buyStock("dcbs", 2000, "dap", 5);
        } catch (StockException e) {
            e.printStackTrace();
        }
        double accountBalance = service.queryAccountBalance("dcbs");
        System.out.println("account balance is " + accountBalance);
    }

Copy the code

Output result:

InsertAccount Connection hashCode =656479172 updateAccount Connection hashCode =517355658 Account balance is 8000.0

The application throws an exception, but accountDao. UpdateAccount commits instead. To find out why, look directly at the Spring source code:

TransactionAspectSupport

protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) { if (txInfo ! = null && txInfo.hasTransaction()) { if (logger.isTraceEnabled()) { logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "] after exception: " + ex); } if (txInfo.transactionAttribute.rollbackOn(ex)) { try { txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); } catch (TransactionSystemException ex2) { logger.error("Application exception overridden by rollback exception", ex); ex2.initApplicationException(ex); throw ex2; }... } public class DefaultTransactionAttribute extends DefaultTransactionDefinition implements TransactionAttribute { @Override public boolean rollbackOn(Throwable ex) { return (ex instanceof RuntimeException || ex instanceof Error); }... }Copy the code

As you can see from the code, Spring transactions rollback only RuntimeException and Error by default. If your application needs to rollback specified exception classes, you can configure the rollback-for= property, for example:

<! --> <tx:advice ID ="txAdvice" transaction-manager="myTxManager"> < TX: Attributes > <! <tx:method name="buyStock" Isolation ="DEFAULT" Propagation ="REQUIRED" rollback-for="StockException"/> </tx:attributes> </tx:advice>Copy the code

Transaction does not roll back

The transaction configuration aspect is not in effect

Exceptions are caught in the application method

Exceptions thrown are not run-time exceptions (such as IOException),

The rollback-for attribute is incorrectly configured

Let’s look at the third pit of Spring transactions:

3.3 Transaction timeout does not take effect

Test code:

<! --> <tx:advice ID ="txAdvice" transaction-manager="myTxManager"> < TX: Attributes > <tx:method name="openAccountForLongTime" isolation="DEFAULT" propagation="REQUIRED" timeout="3"/> </tx:attributes> </tx:advice>Copy the code
@Override public void openAccountForLongTime(String aname, double money) { accountDao.insertAccount(aname, money); try { Thread.sleep(5000L); // Timeout after database operation} catch (InterruptedException e) {e.printStackTrace(); }}Copy the code
@Test
    public void testTimeout() {
        service.openAccountForLongTime("dcbs", 10000);
    }

Copy the code

The transaction timeout did not take effect

public void openAccountForLongTime(String aname, double money) { try { Thread.sleep(5000L); // Timeout before database operation} catch (InterruptedException e) {e.printStackTrace(); } accountDao.insertAccount(aname, money); }Copy the code

Throws a transaction timeout exception, and the timeout takes effect

org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Fri Nov 23 17:03:02 CST 2018 at Org. Springframework. Transaction. Support. ResourceHolderSupport. CheckTransactionTimeout (ResourceHolderSupport. Java: 141)…

The Spring transaction timeout mechanism is used to determine the transaction timeout.

ResourceHolderSupport

/** * Return the time to live for this object in milliseconds. * @return number of millseconds until expiration * @throws TransactionTimedOutException if the deadline has already been reached */ public long getTimeToLiveInMillis() throws TransactionTimedOutException{ if (this.deadline == null) { throw new IllegalStateException("No timeout specified for this resource holder"); } long timeToLive = this.deadline.getTime() - System.currentTimeMillis(); checkTransactionTimeout(timeToLive <= 0); return timeToLive; } /** * Set the transaction rollback-only if the deadline has been reached, * and throw a TransactionTimedOutException. */ private void checkTransactionTimeout(boolean deadlineReached) throws TransactionTimedOutException { if (deadlineReached) { setRollbackOnly(); throw new TransactionTimedOutException("Transaction timed out: deadline was " + this.deadline); }}Copy the code

By looking at the Call Hierarchy of the getTimeToLiveInMillis method, you can see that it is called by applyTimeout of DataSourceUtils. Continue with applyTimeout’s Call Hierarchy, You can see two calls, one is the JdbcTemplate, one is TransactionAwareInvocationHandler class, the latter is only TransactionAwareDataSourceProxy class calls, The DataSource transaction proxy class is not normally used. Does the timeout only apply when the JdbcTemplate is called? Write code to test:

<! --> <tx:advice ID ="txAdvice" transaction-manager="myTxManager"> < TX: Attributes > <tx:method name="openAccountForLongTimeWithoutJdbcTemplate" isolation="DEFAULT" propagation="REQUIRED" timeout="3"/> </tx:attributes> </tx:advice>Copy the code
public void openAccountForLongTimeWithoutJdbcTemplate(String aname, double money) { try { Thread.sleep(5000L); } catch (InterruptedException e) { e.printStackTrace(); } accountDao.queryAccountBalanceWithoutJdbcTemplate(aname); } public double queryAccountBalanceWithoutJdbcTemplate(String aname) { String sql = "select balance from account where aname = ?" ; PreparedStatement prepareStatement; try { prepareStatement = this.getConnection().prepareStatement(sql); prepareStatement.setString(1, aname); ResultSet executeQuery = prepareStatement.executeQuery(); while(executeQuery.next()) { return executeQuery.getDouble(1); } } catch (CannotGetJdbcConnectionException | SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } return 0; }Copy the code

The transaction is running properly, but expires due to transaction timeout

If the JdbcTemplate method is not called after the timeout, it cannot accurately determine the timeout. It is also known that Spring’s transaction timeout is not valid if you are operating on a database such as Mybatis. For this reason, Spring’s transaction timeouts are used with caution.

Four,

Connection setAutoCommit in JDBC specification is the native control manual transaction method, but propagation behavior, exception rollback, Connection management and many other technical issues need to be dealt with by the developer, and Spring transactions through AOP very elegant way to block these technical complexity. Makes transaction management incredibly simple.

But there are pros and cons to everything, and it’s easy to fall into the trap if you don’t understand the implementation mechanism thoroughly. To summarize the possible pitfalls of Spring transactions:

1. The Spring transaction does not take effect

The calling method itself did not configure the transaction correctly

This class method is called directly

The database operation did not get a Connection through Spring’s DataSourceUtils

Multithreaded call

2. Spring transaction rollback failure

The rollback-for attribute is not configured correctly

Exception classes are not RuntimeException and Error

The application caught an exception and did not throw it

3. Spring transaction timeout is inaccurate or invalid

The timeout occurs after the last JdbcTemplate operation

Operate the database with a non-Jdbctemplate, such as Mybatis

Spring series of study notes and interview questions, including Spring interview questions, Spring Cloud interview questions, Spring Boot interview questions, Spring Tutorial notes, Spring Boot tutorial notes, 2020 Java Interview Manual. A total of 1184 pages of PDF documents were organized.