Spring transaction three consecutive questions: What problem does it solve? How to solve? What's the problem?

1. What problem to solve

Let's start with affairs first, "What are affairs? Why do we need affairs?".

A transaction is a set of operations that cannot be divided. Either all operations succeed or all fail. During development, we need to combine some operations into a unit through transactions to ensure the logical correctness of the program, for example, all inserts are successful, or rollbacks, and none of them are inserted. As programmers, what we need to do for transaction management is to define the transaction , that is, to define the beginning and end of the transaction through operations similar to begin transaction and end transaction.

The following is a basic JDBC transaction management code:

// 开启数据库连接
Connection con = openConnection();try {
    // 关闭自动提交
    con.setAutoCommit(false);
    // 业务处理
    // ...  
    // 提交事务
    con.commit();} catch (SQLException | MyException e) {
    // 捕获异常,回滚事务
    try {
        con.rollback();    } catch (SQLException ex) {
        ex.printStackTrace();    }} finally {
    // 关闭连接
    try {
        con.setAutoCommit(true);
        con.close();    } catch (SQLException e) {
        e.printStackTrace();    }}

Intuitively, the code that directly uses JDBC for transaction management has two problems:

  1. Business processing code and transaction management code are mixed;
  2. A lot of exception handling code (try-catch in catch).

And if we need to replace other data access technologies, such as Hibernate, MyBatis, JPA, etc., although the transaction management operations are similar, but the API is different, we need to use the corresponding API to rewrite. This also leads to the third question:

  1. Complex transaction management API.

The three problems to be solved are listed above. Let's see how Spring transactions are solved.

2. How to solve

2.1 Complex transaction management API

In response to this problem, we can easily think of an abstraction on the API of many transaction management. Shield the specific implementation by defining the interface , and then use the strategy mode to determine the specific API. Let's look at the abstract interface defined in Spring transactions.

In Spring transactions, the core interface is PlatformTransactionManager, also called transaction manager, which is defined as follows:

public interface PlatformTransactionManager extends TransactionManager {
    // 获取事务(新的事务或者已经存在的事务)
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
			throws TransactionException;   
    // 提交事务
    void commit(TransactionStatus status) throws TransactionException;
    // 回滚事务
    void rollback(TransactionStatus status) throws TransactionException;
}

getTransaction obtains TransactionStatus by entering TransactionDefinition, that is, creating the corresponding transaction object through the defined transaction meta information. Meta information of the transaction will be included in the TransactionDefinition :

  • PropagationBehavior: Propagation behavior;
  • IsolationLevel: isolation level;
  • Timeout: timeout time;
  • ReadOnly: Whether it is read-only.

The TransactionStatus obtained according to the TransactionDefinition will encapsulate the transaction object and provide methods to manipulate the transaction and view the transaction status , for example:

  • setRollbackOnly: Mark the transaction as Rollback-only so that it can be rolled back;
  • isRollbackOnly: Check whether it is marked as Rollback-only;
  • isCompleted: Check whether the transaction has been completed (commit or rollback completed).

Related methods for nested transactions are also supported:

  • createSavepoint:创建savepoint;
  • rollbackToSavepoint: roll back to the specified savepoint;
  • releaseSavePoint:释放savepoint。

The TransactionStatus transaction object can be passed into the commit method or rollback method to complete the commit or rollback of the transaction.

Below we understand the role of TransactionStatus through a specific implementation. Take the commit method as an example, how to complete the transaction submission through TransactionStatus. AbstractPlatformTransactionManager is an implementation of the PlatformTransactionManager interface. As a template class, its commit implementation is as follows:

public final void commit(TransactionStatus status) throws TransactionException {
    // 1.检查事务是否已完成
    if (status.isCompleted()) {
        throw new IllegalTransactionStateException(            "Transaction is already completed - do not call commit or rollback more than once per transaction");
    }    // 2.检查事务是否需要回滚(局部事务回滚)
    DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
    if (defStatus.isLocalRollbackOnly()) {
        if (defStatus.isDebug()) {
            logger.debug("Transactional code has requested rollback");
        }        processRollback(defStatus, false);
        return;
    }    // 3.检查事务是否需要回滚(全局事务回滚)
    if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
        if (defStatus.isDebug()) {
            logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
        }        processRollback(defStatus, true);
        return;
    }        // 4.提交事务
    processCommit(defStatus);}

The basic logic of transaction submission is defined in the commit template method . By checking the status of the transaction, you can decide whether to throw an exception, rollback, or submit. The processRollback and processCommit methods are also template methods, which further define the logic of rollback and submission. Taking the processCommit method as an example, the specific commit operation will be completed by the abstract method doCommit.

protected abstract void doCommit(DefaultTransactionStatus status) throws TransactionException;

The realization of doCommit depends on the specific data access technology. Let's look at the implementation of doCommit in the corresponding concrete implementation class of JDBC DataSourceTransactionManager.

protected void doCommit(DefaultTransactionStatus status) {
    // 获取status中的事务对象    
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
    // 通过事务对象获得数据库连接对象    Connection con = txObject.getConnectionHolder().getConnection();    if (status.isDebug()) {
        logger.debug("Committing JDBC transaction on Connection [" + con + "]");
    }    try {        // 执行commit        con.commit();    }    catch (SQLException ex) {        throw new TransactionSystemException("Could not commit JDBC transaction", ex);
    }}

In the commit and processCommit methods, we determine the transaction behavior according to the transaction status provided by the input TransactionStatus , and when the transaction commit needs to be executed in doCommit, the database connection object will be obtained through the transaction object in TransactionStatus , and then the final commit operation . Through this example, we can understand the transaction status and the role of the transaction object provided by TransactionStatus.

The following is the transaction management code rewritten with Spring transaction API:

// 获得事务管理器
PlatformTransactionManager txManager = getPlatformTransactionManager();DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// 指定事务元信息
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);// 获得事务
TransactionStatus status = txManager.getTransaction(def);try {
    // 业务处理
}catch (MyException ex) {
    // 捕获异常,回滚事务
    txManager.rollback(status);    throw ex;
}// 提交事务
txManager.commit(status);

Whether using JDBC, Hibernate or MyBatis, we only need to pass the corresponding specific implementation to txManager to switch between multiple data access technologies.

Summary: Spring transactions unify the transaction management API through the PlatformTransactionManager, TransactionDefinition and TransactionStatus interfaces, and combine the strategy mode and template method to determine the specific implementation.

Have you noticed another feature of the Spring transaction API code? SQLException is gone. Let's take a look at how Spring transactions solve a large amount of exception handling code.

2.2 A lot of exception handling code

Why do you need to write so much exception handling code in the code that uses JDBC. This is because each method of Connection throws SQLException, and SQLException is a checked exception , which forces us to perform exception handling when using its methods. How does Spring transaction solve this problem? Let's look at the doCommit method:

protected void doCommit(DefaultTransactionStatus status) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();    Connection con = txObject.getConnectionHolder().getConnection();    if (status.isDebug()) {
        logger.debug("Committing JDBC transaction on Connection [" + con + "]");
    }    try {
        con.commit();    }    catch (SQLException ex) {
        // 异常转换
        throw new TransactionSystemException("Could not commit JDBC transaction", ex);
    }
}

Connection's commit method will throw a checked exception SQLException, SQLException will be converted into TransactionSystemException thrown in the catch code block, and TransactionSystemException is a non-checked exception. By converting checked exceptions into non-checked exceptions , we can decide for ourselves whether to catch exceptions and not enforce exception handling.

Spring transaction defines corresponding exceptions for almost all errors in the database, and unified different exception APIs such as JDBC, Hibernate, and MyBatis. This helps us to use a unified exception API interface when handling exceptions, without worrying about specific data access technologies.

Summary: Spring transactions avoid forced exception handling through exception conversion.

2.3 Miscellaneous business processing code and transaction management code

In section 2.1, the writing method of using Spring transaction API, namely programmatic transaction management, is given, but the problem of "mixed business processing code and transaction management code" is still not solved. At this time, Spring AOP can be used to separate the cross-cutting concern of transaction management code from the code, that is, declarative transaction management . Take the annotation method as an example, by marking the method with @Transaction annotation, it will provide transaction management for the method. The principle is shown in the figure below:

Three questions about Spring transaction: What problem does it solve?  How to solve?  What's the problem?

 

Spring transaction will generate AOP-enhanced dynamic proxy class objects for the classes of methods annotated by @Transaction, and add TransactionInterceptor to the interception chain that calls the target method to increase the surrounding and realize transaction management.

Let's look at the specific implementation in TransactionInterceptor. In its invoke method, invokeWithinTransaction method will be called for transaction management, as shown below:

protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
        throws Throwable {    // 查询目标方法事务属性、确定事务管理器、构造连接点标识(用于确认事务名称)
    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)) {
        // 创建事务
        TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);        Object retVal = null;
        try {
            // 通过回调执行目标方法
            retVal = invocation.proceedWithInvocation();        }        catch (Throwable ex) {
            // 目标方法执行抛出异常,根据异常类型执行事务提交或者回滚操作
            completeTransactionAfterThrowing(txInfo, ex);            throw ex;
        }        finally {
            // 清理当前线程事务信息
            cleanupTransactionInfo(txInfo);        }        // 目标方法执行成功,提交事务
        commitTransactionAfterReturning(txInfo);        return retVal;
    } else {
        // 带回调的事务执行处理,一般用于编程式事务
        // ...
    }}

Before and after calling the target method, operations such as creating transactions, handling exceptions, and committing transactions are added. This saves us from having to write transaction management code, just specify transaction-related meta-information through @Transaction attributes.

Summary: Spring transactions provide declarative transactions through AOP to separate business processing code and transaction management code.

3. What's the problem

Spring transaction solves the three problems listed in the first section for us, but it also brings some new problems.

3.1 Failure of non-public methods

@Transactional can only take effect when marked on public-level methods, and will not take effect for non-public methods. This is because Spring AOP does not support interception of private and protect methods. In principle, the dynamic proxy is implemented through the interface, so naturally it cannot support the private and protect methods. The CGLIB is implemented through inheritance. In fact, it can support the interception of the protect method, but Spring AOP does not support such use. The author guesses that this restriction is due to the consideration that the proxy method should be public, and to maintain CGLIB and dynamic proxy Consistent. If you need to intercept protect or private methods, it is recommended to use AspectJ.

3.2 Self-call failure

When the method with @Transactional is directly called through the internal method of Bean, @Transactional will be invalid, for example:

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(b);}

Call the saveA and saveB methods in saveAB, the @Transactional of both will be invalid. This is because the implementation of Spring transactions is based on the proxy class. When the method is directly called internally, it will not go through the proxy object, but directly call the method of the target object, which cannot be intercepted and processed by the TransactionInterceptor. Solution:

(1)ApplicationContextAware

The proxy object is obtained through the context injected by ApplicationContextAware.

public void saveAB(A a, B b)
{    Test self = (Test) applicationContext.getBean("Test");
    self.saveA(a);
    self.saveB(b);
}

(2) AopContext

Obtain the proxy object through AopContext.

public void saveAB(A a, B b)
{    Test self = (Test)AopContext.currentProxy();
    self.saveA(a);
    self.saveB(b);
}

(3)@Autowired

Inject proxy objects through the @Autowired annotation.

@Component
public class Test {
    @Autowired
    Test self;    public void saveAB(A a, B b)
    {
        self.saveA(a);        self.saveB(b);    }    // ...
}

(4) Split

Split the saveA and saveB methods into another class.

public void saveAB(A a, B b)
{    txOperate.saveA(a);    txOperate.saveB(b);}

The above two problems are caused by the limitations of Spring transaction implementation. Let's look at two more problems that are easy to make mistakes due to improper use .

3.3 Check exceptions are not rolled back by default

By default, throwing an unchecked exception will trigger a rollback, while a checked exception will not.

According to the invokeWithinTransaction method, we can know that the exception handling logic is in the completeTransactionAfterThrowing method, which is implemented as follows:

protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
    if (txInfo != null && txInfo.getTransactionStatus() != null) {
        if (logger.isTraceEnabled()) {
            logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
                         "] after exception: " + ex);
        }        if (txInfo.transactionAttribute != null && 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;
            }            catch (RuntimeException | Error ex2) {
                logger.error("Application exception overridden by rollback exception", ex);
                throw ex2;
            }        }        else {
            try {
                // 异常类型为非回滚异常,仍然执行事务提交
                txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());            }            catch (TransactionSystemException ex2) {
                logger.error("Application exception overridden by commit exception", ex);
                ex2.initApplicationException(ex);                throw ex2;
            }            catch (RuntimeException | Error ex2) {
                logger.error("Application exception overridden by commit exception", ex);
                throw ex2;
            }        }    }}

Determine whether the exception is a rollback exception based on rollbackOn. Only instances of RuntimeException and Error, that is, unchecked exceptions, or the type of rollback exception specified by the rollbackFor attribute in @Transaction, will rollback the transaction. Otherwise, the transaction will continue to be committed. So if you need to roll back unchecked exceptions, you need to remember to specify the rollbackFor attribute, otherwise the rollback will be invalid.

3.4 Catch exception cannot be rolled back

In Section 3.3 we said that only throwing unchecked exceptions or exceptions specified in rollbackFor can trigger a rollback. If we catch the exception without throwing it, it will fail to trigger a rollback, which is also a common mistake in development. E.g:

@Transactional
public void insert(List<User> users) {
    try {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        for (User user : users) {
            String insertUserSql = "insert into User (id, name) values (?,?)";
            jdbcTemplate.update(insertUserSql, new Object[] { user.getId(),
                                                             user.getName() });        }    } catch (Exception e) {
        e.printStackTrace();    }}

Here, all Exception was caught because of the catch, and it was not thrown. When an exception occurs in the insert, the rollback will not be triggered.

But at the same time, we can also use this mechanism to use try-catch to wrap data operations that do not participate in transactions. For example, for writing some unimportant logs, we can wrap them with try-catch to avoid throwing exceptions. Failure to write the log affects the commit of the transaction.

Author: Pinch grass child
Source: Nuggets

Guess you like

Origin blog.csdn.net/qq_45401061/article/details/108607490