天天看點

61張圖,圖解Spring事務,拆解底層源碼

作者:IT網際網路新資訊

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

不 BB,上文章目錄。

61張圖,圖解Spring事務,拆解底層源碼

1. 項目準備

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

下面是 DB 資料和 DB 操作接口:

uidunameusex1張三女2陳恒男3樓仔男

// 提供的接口
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 塊,我們會結合上面的示例,給大家進行講解。

61張圖,圖解Spring事務,拆解底層源碼

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

擷取 Louzai 的切面方法:首先會拿到所有的切面資訊,和 Louzai 的所有方法進行比對,然後找到 Louzai 所有需要進行事務處理的方法,比對成功的方法,還需要将事務屬性儲存到緩存 attributeCache 中。

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

61張圖,圖解Spring事務,拆解底層源碼

第二塊是事務執行,整個邏輯比較複雜,我隻選取 4 塊最核心的邏輯,分别為從緩存拿到事務屬性、建立并開啟事務、執行業務邏輯、送出或者復原事務。

3. 源碼解讀

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

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

3.1 代碼入口

61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼

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

61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼

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

61張圖,圖解Spring事務,拆解底層源碼

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

61張圖,圖解Spring事務,拆解底層源碼

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

61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼

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

3.2 建立代理對象

61張圖,圖解Spring事務,拆解底層源碼

這裡是重點!敲黑闆!!!

  1. 先擷取 louzai 類的所有切面清單;
  2. 建立一個 AOP 的代理對象。
61張圖,圖解Spring事務,拆解底層源碼

3.2.1 擷取切面清單

61張圖,圖解Spring事務,拆解底層源碼

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

61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼

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

61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼

進入 canApply(),開始比對 louzai 的切面。

61張圖,圖解Spring事務,拆解底層源碼

這裡是重點!敲黑闆!!!

這裡隻會比對到 Louzai.testSuccess() 方法,我們直接進入比對邏輯。

如果比對成功,還會把事務的屬性配置資訊放入 attributeCache 緩存。

61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼

我們依次傳回到 getTransactionAttribute(),再看看放入緩存中的資料。

61張圖,圖解Spring事務,拆解底層源碼

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

61張圖,圖解Spring事務,拆解底層源碼

3.2.2 建立 AOP 代理對象

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

3.3 事務執行

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

61張圖,圖解Spring事務,拆解底層源碼

因為代理對象是 Cglib 方式建立,是以通過 Cglib 來執行。

61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼

這裡是重點!敲黑闆!!!

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

61張圖,圖解Spring事務,拆解底層源碼
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() 中,我們找到擷取事務屬性的入口。

61張圖,圖解Spring事務,拆解底層源碼

從 attributeCache 擷取事務的緩存資料,緩存資料是在 “2.2.1 擷取切面清單” 中儲存的。

61張圖,圖解Spring事務,拆解底層源碼

3.3.2 建立事務

61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼

通過 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() 開啟事務。

61張圖,圖解Spring事務,拆解底層源碼

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

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 對象。

61張圖,圖解Spring事務,拆解底層源碼

3.3.3 執行邏輯

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

61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼

進入到真正的業務邏輯。

61張圖,圖解Spring事務,拆解底層源碼

執行完畢後抛出異常,依次傳回,走後續的復原事務邏輯。

3.3.4 復原事務

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

61張圖,圖解Spring事務,拆解底層源碼

61張圖,圖解Spring事務,拆解底層源碼

執行復原邏輯很簡單,我們隻看如何判斷是否復原。

61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼
61張圖,圖解Spring事務,拆解底層源碼

如果抛出的異常類型,和事務定義的異常類型比對,證明該異常需要捕獲。

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

61張圖,圖解Spring事務,拆解底層源碼

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

4. 結語

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

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

  • 先比對出 louzai 對象所有關于事務的切面清單,并将比對成功的事務屬性儲存到緩存;
  • 從緩存取出事務屬性,然後建立、啟動事務,執行業務邏輯,最後送出或者復原事務。

原文:https://docs.qq.com/doc/DQ3RjTXVmek9Ca2Jw

作者:樓仔

如果感覺本文對你有幫助,點贊關注支援一下