61 張圖,剖析 Spring 事務,就是要鑽到底!

大家好,我是樓仔!

我終於知道爲什麼很少有博主願意寫源碼系列的文章,真的太熬人,這個是源碼系列的第 4 篇,感覺人都快被熬廢了。

這篇源碼解析,和 Spring AOP 中的知識有很多重合的地方,但是比 AOP 要稍微簡單一些,建議兩篇文章對比學習。

下面我會簡單介紹一下 Spring 事務的基礎知識,以及使用方法,然後直接對源碼進行拆解。

不 BB,上文章目錄。

1. 項目準備

需要搭建環境的同學,代碼詳見:https://github.com/lml200701158/program_demo/tree/main/spring-transaction

下面是 DB 數據和 DB 操作接口:

qVoSfx

// 提供的接口
public interface UserDao {
    // select * from user_test where uid = "#{uid}"
    public MyUser selectUserById(Integer uid);
    // update user_test set uname =#{uname},usex = #{usex} where uid = #{uid}
    public int updateUser(MyUser user);
}

基礎測試代碼,testSuccess() 是事務生效的情況:

@Service
public class Louzai {
    @Autowired
    private UserDao userDao;

    public void update(Integer id) {
        MyUser user = new MyUser();
        user.setUid(id);
        user.setUname("張三-testing");
        user.setUsex("女");
        userDao.updateUser(user);
    }

    public MyUser query(Integer id) {
        MyUser user = userDao.selectUserById(id);
        return user;
    }

    // 正常情況
    @Transactional(rollbackFor = Exception.class)
    public void testSuccess() throws Exception {
        Integer id = 1;
        MyUser user = query(id);
        System.out.println("原記錄:" + user);
        update(id);
        throw new Exception("事務生效");
    }
}

執行入口:

public class SpringMyBatisTest {
    public static void main(String[] args) throws Exception {
        String xmlPath = "applicationContext.xml";
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
        Louzai uc = (Louzai) applicationContext.getBean("louzai");
        uc.testSuccess();
    }
}

輸出:

16:44:38.267 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.transaction.interceptor.TransactionInterceptor#0'
16:44:38.363 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'txManager'
16:44:40.966 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Creating new transaction with name [com.mybatis.controller.Louzai.testSuccess]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-java.lang.Exception
16:44:40.968 [main] DEBUG org.springframework.jdbc.datasource.DriverManagerDataSource - Creating new JDBC DriverManager Connection to [jdbc:mysql://127.0.0.1:3306/java_study?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai]
16:44:41.228 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@5b5caf08] for JDBC transaction
16:44:41.231 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5b5caf08] to manual commit
原記錄:MyUser(uid=1, uname=張三, usex=)
16:42:59.345 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Initiating transaction rollback
16:42:59.346 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@70807224]
16:42:59.354 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@70807224] after transaction
Exception in thread "main" java.lang.Exception: 事務生效
 at com.mybatis.controller.Louzai.testSuccess(Louzai.java:34)
  // 異常日誌省略...

2. Spring 事務工作流程

爲了方便大家能更好看懂後面的源碼,我先整體介紹一下源碼的執行流程,讓大家有一個整體的認識,否則容易被繞進去。

整個 Spring 事務源碼,其實分爲 2 塊,我們會結合上面的示例,給大家進行講解。

第一塊是後置處理,我們在創建 Louzai Bean 的後置處理器中,裏面會做兩件事情:

獲取 Louzai 的切面方法:首先會拿到所有的切面信息,和 Louzai 的所有方法進行匹配,然後找到 Louzai 所有需要進行事務處理的方法,匹配成功的方法,還需要將事務屬性保存到緩存 attributeCache 中。

創建 AOP 代理對象:結合 Louzai 需要進行 AOP 的方法,選擇 Cglib 或 JDK,創建 AOP 代理對象。

第二塊是事務執行,整個邏輯比較複雜,我只選取 4 塊最核心的邏輯,分別爲從緩存拿到事務屬性、創建並開啓事務、執行業務邏輯、提交或者回滾事務。

3. 源碼解讀

注意:Spring 的版本是 5.2.15.RELEASE,否則和我的代碼不一樣!!!

上面的知識都不難,下面纔是我們的重頭戲,讓你跟着樓仔,走一遍代碼流程。

3.1 代碼入口

這裏需要多跑幾次,把前面的 beanName 跳過去,只看 louzai。

進入 doGetBean(),進入創建 Bean 的邏輯。

進入 createBean(),調用 doCreateBean()。

進入 doCreateBean(),調用 initializeBean()。

如果看過我前面幾期系列源碼的同學,對這個入口應該會非常熟悉,其實就是用來創建代理對象。

3.2 創建代理對象

這裏是重點!敲黑板!!!

  1. 先獲取 louzai 類的所有切面列表;

  2. 創建一個 AOP 的代理對象。

3.2.1 獲取切面列表

這裏有 2 個重要的方法,先執行 findCandidateAdvisors(),待會我們還會再返回 findEligibleAdvisors()。

依次返回,重新來到 findEligibleAdvisors()。

進入 canApply(),開始匹配 louzai 的切面。

這裏是重點!敲黑板!!!

這裏只會匹配到 Louzai.testSuccess() 方法,我們直接進入匹配邏輯。

如果匹配成功,還會把事務的屬性配置信息放入 attributeCache 緩存。

我們依次返回到 getTransactionAttribute(),再看看放入緩存中的數據。

再回到該小節開頭,我們拿到 louzai 的切面信息,去創建 AOP 代理對象。

3.2.2 創建 AOP 代理對象

創建 AOP 代理對象的邏輯,在上一篇文章(Spring AOP)講解過,我是通過 Cglib 創建,感興趣的同學可以關注公衆號「樓仔」,翻一下樓仔的歷史文章。

3.3 事務執行

回到業務邏輯,通過 louzai 的 AOP 代理對象,開始執行主方法。

因爲代理對象是 Cglib 方式創建,所以通過 Cglib 來執行。

這裏是重點!敲黑板!!!

下面的代碼是事務執行的核心邏輯 invokeWithinTransaction()。

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
            final InvocationCallback invocation) throws Throwable {

        //獲取我們的事務屬源對象
        TransactionAttributeSource tas = getTransactionAttributeSource();
        //通過事務屬性源對象獲取到我們的事務屬性信息
        final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
        //獲取我們配置的事務管理器對象
        final PlatformTransactionManager tm = determineTransactionManager(txAttr);
        //從tx屬性對象中獲取出標註了@Transactionl的方法描述符
        final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

        //處理聲明式事務
        if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
            //有沒有必要創建事務
            TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);

            Object retVal;
            try {
                //調用鉤子函數進行回調目標方法
                retVal = invocation.proceedWithInvocation();
            }
            catch (Throwable ex) {
                //拋出異常進行回滾處理
                completeTransactionAfterThrowing(txInfo, ex);
                throw ex;
            }
            finally {
                //清空我們的線程變量中transactionInfo的值
                cleanupTransactionInfo(txInfo);
            }
            //提交事務
            commitTransactionAfterReturning(txInfo);
            return retVal;
        }
        //編程式事務
        else {
          // 這裏不是我們的重點,省略...
        }
    }

3.3.1 獲取事務屬性

在 invokeWithinTransaction() 中,我們找到獲取事務屬性的入口。

從 attributeCache 獲取事務的緩存數據,緩存數據是在 “2.2.1 獲取切面列表” 中保存的。

3.3.2 創建事務

通過 doGetTransaction() 獲取事務。

protected Object doGetTransaction() {
        //創建一個數據源事務對象
        DataSourceTransactionObject txObject = new DataSourceTransactionObject();
        //是否允許當前事務設置保持點
        txObject.setSavepointAllowed(isNestedTransactionAllowed());
        /**
         * TransactionSynchronizationManager 事務同步管理器對象(該類中都是局部線程變量)
         * 用來保存當前事務的信息,我們第一次從這裏去線程變量中獲取 事務連接持有器對象 通過數據源爲key去獲取
         * 由於第一次進來開始事務 我們的事務同步管理器中沒有被存放.所以此時獲取出來的conHolder爲null
         */
        ConnectionHolder conHolder =
                (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());
        txObject.setConnectionHolder(conHolder, false);
        //返回事務對象
        return txObject;
    }

通過 startTransaction() 開啓事務。

下面是開啓事務的詳細邏輯,瞭解一下即可。

protected void doBegin(Object transaction, TransactionDefinition definition) {
        //強制轉化事務對象
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
        Connection con = null;

        try {
            //判斷事務對象沒有數據庫連接持有器
            if (!txObject.hasConnectionHolder() ||
                    txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
                //通過數據源獲取一個數據庫連接對象
                Connection newCon = obtainDataSource().getConnection();
                if (logger.isDebugEnabled()) {
                    logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
                }
                //把我們的數據庫連接包裝成一個ConnectionHolder對象 然後設置到我們的txObject對象中去
                txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
            }

            //標記當前的連接是一個同步事務
            txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
            con = txObject.getConnectionHolder().getConnection();

            //爲當前的事務設置隔離級別
            Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
            txObject.setPreviousIsolationLevel(previousIsolationLevel);

            //關閉自動提交
            if (con.getAutoCommit()) {
                txObject.setMustRestoreAutoCommit(true);
                if (logger.isDebugEnabled()) {
                    logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
                }
                con.setAutoCommit(false);
            }

            //判斷事務爲只讀事務
            prepareTransactionalConnection(con, definition);
            //設置事務激活
            txObject.getConnectionHolder().setTransactionActive(true);

            //設置事務超時時間
            int timeout = determineTimeout(definition);
            if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
                txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
            }

            // 綁定我們的數據源和連接到我們的同步管理器上   把數據源作爲key,數據庫連接作爲value 設置到線程變量中
            if (txObject.isNewConnectionHolder()) {
                TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
            }
        }

        catch (Throwable ex) {
            if (txObject.isNewConnectionHolder()) {
                //釋放數據庫連接
                DataSourceUtils.releaseConnection(con, obtainDataSource());
                txObject.setConnectionHolder(null, false);
            }
            throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
        }
    }

最後返回到 invokeWithinTransaction(),得到 txInfo 對象。

3.3.3 執行邏輯

還是在 invokeWithinTransaction() 中,開始執行業務邏輯。

進入到真正的業務邏輯。

執行完畢後拋出異常,依次返回,走後續的回滾事務邏輯。

3.3.4 回滾事務

還是在 invokeWithinTransaction() 中,進入回滾事務的邏輯。

執行回滾邏輯很簡單,我們只看如何判斷是否回滾。

如果拋出的異常類型,和事務定義的異常類型匹配,證明該異常需要捕獲。

之所以用遞歸,不僅需要判斷拋出異常的本身,還需要判斷它繼承的父類異常,滿足任意一個即可捕獲。

到這裏,所有的流程結束。

4. 結語

我們再小節一下,文章先介紹了事務的使用示例,以及事務的執行流程。

之後再剖析了事務的源碼,分爲 2 塊:

這篇文章,是 Spring 源碼解析的第 4 篇,如果之前已經看過 AOP 的源碼解析,這篇就要容易很多,但是如果上來就直接肝,可能會有那麼一丟丟難度。

源碼系列共有 5 篇,你猜猜最後一篇,我會寫什麼呢,可以給樓仔留言哈。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/5HVpCjouOFsLfvC93XfLzw