1. 程式人生 > >mybatis原始碼解析(六)-配合spring-tx實現事務的原理

mybatis原始碼解析(六)-配合spring-tx實現事務的原理

spring-tx關鍵物件

物件包含關係

TransactionAspectSupport.TransactionInfo包含TransactionStatus物件,TransactionStatus物件包含DataSourceTransactionManager.DataSourceTransactionObject(簡稱Transaction物件),DataSourceTransactionManager.DataSourceTransactionObject包含ConnectionHolder物件,ConnectionHolder包含Connection。

物件建立關係

建立ConnectionHolder需要DataSource,所以DataSourceTransactionManager建立ConnectionHolder。
其他物件基本是在TransactionAspectSupport

DataSourceTransactionManager這兩個類中。
這些物件基本都是在before advice過程中建立的。

物件存放的位置

TransactionAspectSupport裡的ThreadLocal維護TransactionAspectSupport.TransactionInfo。 TransactionSynchronizationManager裡的ThreadLocal維和執行緒相關的TransactionStatus。
其他物件在引用它的物件中。

spring-tx原理

首先spring-tx是基於aop的,既然aop,關鍵就在advice裡。增強後的程式碼是這個樣子的:

Code1
org.springframework.transaction.interceptor.TransactionAspectSupport中

    TransactionAspectSupport.TransactionInfo txInfo = this.createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
    Object retVal = null;
    try {
        retVal = invocation.proceedWithInvocation();
    } catch
(Throwable var15) { this.completeTransactionAfterThrowing(txInfo, var15); throw var15; } finally { this.cleanupTransactionInfo(txInfo); } this.commitTransactionAfterReturning(txInfo); return retVal;

Code1第1行,會建立一個新的TransactionStatus,並放在適當的ThreadLocal裡,建立TransactionStatus的過程處理的情況比較多,比如現在是否是在Transaction中、新的propagation是怎樣的。我們設定的propagation的值也是在這裡生效的,這部分程式碼會根據propagation的不同做一些不同的操作(比如是否新建Connection,是否開啟新的Transaction)。
建立TransactionStatus的程式碼如下:

Code2
org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction

    public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
        Object transaction = this.doGetTransaction();
        boolean debugEnabled = this.logger.isDebugEnabled();
        if (definition == null) {
            definition = new DefaultTransactionDefinition();
        }
        if (this.isExistingTransaction(transaction)) {
            return this.handleExistingTransaction((TransactionDefinition)definition, transaction, debugEnabled);
        } else if (((TransactionDefinition)definition).getTimeout() < -1) {
            throw new InvalidTimeoutException("Invalid transaction timeout", ((TransactionDefinition)definition).getTimeout());
        } else if (((TransactionDefinition)definition).getPropagationBehavior() == 2) {
            throw new IllegalTransactionStateException("No existing transaction found for transaction marked with propagation 'mandatory'");
        } else if (((TransactionDefinition)definition).getPropagationBehavior() != 0 && ((TransactionDefinition)definition).getPropagationBehavior() != 3 && ((TransactionDefinition)definition).getPropagationBehavior() != 6) {
            if (((TransactionDefinition)definition).getIsolationLevel() != -1 && this.logger.isWarnEnabled()) {
                this.logger.warn("Custom isolation level specified but no actual transaction initiated; isolation level will effectively be ignored: " + definition);
            }
            boolean newSynchronization = this.getTransactionSynchronization() == 0;
            return this.prepareTransactionStatus((TransactionDefinition)definition, (Object)null, true, newSynchronization, debugEnabled, (Object)null);
        } else {
            AbstractPlatformTransactionManager.SuspendedResourcesHolder suspendedResources = this.suspend((Object)null);
            if (debugEnabled) {
                this.logger.debug("Creating new transaction with name [" + ((TransactionDefinition)definition).getName() + "]: " + definition);
            }
            try {
                boolean newSynchronization = this.getTransactionSynchronization() != 2;
                DefaultTransactionStatus status = this.newTransactionStatus((TransactionDefinition)definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
                this.doBegin(transaction, (TransactionDefinition)definition);
                this.prepareSynchronization(status, (TransactionDefinition)definition);
                return status;
            } catch (RuntimeException var7) {
                this.resume((Object)null, suspendedResources);
                throw var7;
            } catch (Error var8) {
                this.resume((Object)null, suspendedResources);
                throw var8;
            }
        }
    }

Code1第4行就是呼叫被代理物件的方法。

Code1第6行是處理異常的,如果丟擲的異常是@Transaction回滾的,則進行適當的回滾操作(包括真正的回滾或者標記回滾,後面會解釋);如果丟擲的異常不在回滾範圍,那麼這裡依然會執行commit操作,所以我們一定要注意異常型別,尤其是spring-tx配合別的資料框orm框架使用時(後面也會煮個栗子)。

Code1第9行,清理掉ThreadLocal裡的TransactionAspectSupport.TransactionInfo,需要把TransactionAspectSupport.TransactionInfo設定成前一個TransactionAspectSupport.TransactionInfo,這裡有一個鏈式的用法有一點巧妙。事情是這個樣子的:呼叫A.a方法時,設定TransactionAspectSupport.TransactionInfo物件tia在ThreadLocal,在A.a方法中又呼叫了B.b方法,這個方法也是事務的,這時,需要構造一個TransactionAspectSupport.TransactionInfo物件tib放在ThreadLocal裡。且tib有一個屬性叫oldTransactionInfo指向了tia。當B.b方法執行完了,需要把tib在ThreadLocal裡清除,把tia放進ThreadLocal裡,這樣通過tib的oldTransactionInfo屬性可以找到tia。以此類推,鏈式形成。

Code1第11行,當業務程式碼執行完成時,根據情況進行commit或者rollback,這裡和Code1第6行的程式碼是相似的,只是那個是有異常發生時。這裡真正處理是commit還是rollback的程式碼在TransactionManager裡的,呼叫程式碼如下:

Code3
org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning

    if (txInfo != null && txInfo.hasTransaction()) {
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
        }
        txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
    }

propagation

@Transaction裡有幾個引數可以設定,比較關鍵的一個就是propagation,就是傳播規則。
有下面這幾種:

    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);

具體含義可以查詢相關api,我在這裡就敘述一下REQUIREDREQUIRES_NEW是如何生效的。

還是剛才那個例子:
已經呼叫了A.a方法,A.a是被@Transaction註解的方法,在A.a中呼叫了B.b方法,B.b也是@Transaction註解的方法,下面分類討論:

  • 如果B.b的註解為@Transactional(propagation = Propagation.REQUIRES_NEW),則新建立的TransactionStatusnewTransaction屬性為true,同時新建一個Connection,begin transaction,在方法執行完成後會commit或者rollback。
  • 如果B.b的註解為@Transactional(propagation = Propagation.REQUIRED),則新建立的TransactionStatusnewTransaction屬性為false,不會建立新的Connection,使用之前的事務環境,在方法執行完成後不會進行commit或者rollback,如果發生了異常,給TransactionStatus設定一個狀態表示需要回滾,在最外層的方法中會根據這個狀態進行回滾。設定這個狀態如下:

Code4
org.springframework.jdbc.datasourceDataSourceTransactionManager.doSetRollbackOnly

    protected void doSetRollbackOnly(DefaultTransactionStatus status) {
        DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)status.getTransaction();
        txObject.setRollbackOnly();
    }

mybatis配合spring-tx是如何生效的?

萬變不離其宗,不論上層新增幾個概念,java的單機事務都是以jdbc的api為基礎的,關鍵就是同一個Connection的begin/commit/rollback。

spring-tx的原理上面已經描述過了,這裡做一個簡化:
在before advice中,先獲取TransactionManagerTransactionManager中包含DataSource,再根據當前的事務環境和@Transaction.propagation構造出Connection,放在ThreadLocal裡的Map裡,Map的key是DataSource。然後執行業務程式碼。在return advice或exception advice中,先獲取之前生成的Connection,然後根據異常情況以及@Transaction.propagation(並不是直接使用這個屬性,而是使用它轉化後的資訊,比如newTransaction的值,這個屬性在before advice已經轉化到TransactionStatus裡了)進行commit或者rollback(或等價操作,設定標記,等外層commit或者rollback)。

spring-tx生效的關鍵:

1.同一個Connection。

Connection放在ThreadLocal中的Map裡,Map的key為DataSource。所以確保同一個Connection,需要在同一個執行緒中,這個沒有問題,需要使用同一個DataSource去Map裡取。我們自己去寫這幾行程式碼是比較麻煩的,Spring-tx給我們提供了一個工具類來做這個事情,public static Connection org.springframework.jdbc.datasourc.DataSourceUtils.getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException,通過這個方法,只要執行緒和DataSource正確,就可以獲取到正確的Connection。我們使用Connection進行增刪改查,且不執行與Transaction相關的方法,advice去完成Transaction。

我們使用Mybatis-spring這個框架整合一下mybatis和spring,mybatis在@Transaction註解標記的方法內是正常的支援事務的。那麼mybatis是如何獲取到正確的Connection的呢?

在我的mybatis原始碼解析(五)-mybatis如何實現的事務控制這篇文章中可以獲取如下的資訊:使用者操作org.apache.ibatis.session.SqlSession這個類,org.apache.ibatis.session.SqlSession操作org.apache.ibatis.executor.Executor去執行sql,org.apache.ibatis.executor.Executor只是使用Connection,但是不去維護Connection,它維護了org.apache.ibatis.transaction.Transaction,由org.apache.ibatis.transaction.Transaction去建立、維護、回收Connection。
在mybatis-spring初始化的過程會執行如下的程式碼:

Code5
org.mybatis.spring.SqlSessionFactoryBean

    if (this.transactionFactory == null) {
        this.transactionFactory = new SpringManagedTransactionFactory();
    }
    Environment environment = new Environment(this.environment, this.transactionFactory, this.dataSource);
    configuration.setEnvironment(environment);

Code5給Configuration設定了SpringManagedTransactionFactorySpringManagedTransactionFactory就是用來生成Transaction的,它生成的類為org.mybatis.spring.transaction.SpringManagedTransactionorg.mybatis.spring.transaction.SpringManagedTransaction生成Connection的程式碼如下:

Code6
org.mybatis.spring.transaction.SpringManagedTransaction

    private void openConnection() throws SQLException {
        this.connection = DataSourceUtils.getConnection(this.dataSource);
        this.autoCommit = this.connection.getAutoCommit();
        this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
        if (logger.isDebugEnabled()) {
            logger.debug("JDBC Connection [" + this.connection + "] will" + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring");
        }
    }

Code6第2行,就是使用的spring的方法去獲取到對應的Connection了。這就對上了。

2.異常型別匹配,預設是RunTimeException。

@Transaction註解預設回滾的是RunTimeException,為了更方便地配合@Transaction註解,Mybatis預設丟擲的是RunTimeException,這點是比較符合的。關於異常會在後面section描述。

jdbc proxy or cglib

spring-tx是基於spring aop的,aop對於proxy模式,支援jdbc proxy和cglib。

proxy-target-class=”false”(jdbc proxy)

  • 1.註解只放在interface的方法上,jdbc代理,事務生效。
  • 2.註解只放在實現類的方法上,jdbc代理,事務生效。

proxy-target-class=”true”(cglib)

  • 1.註解只放在interface的方法上,cglib代理,事務不生效。
  • 2.註解只放在實現類的方法上,cglib代理,事務生效。cglib代理,事務生效。

使用時,預設的proxy-target-class=”false”就能滿足我們的需求;如果需要使用proxy-target-class=”true”,需要把@Transaction註解加在實現類的方法上,因為註解是不支援繼承的。

注意:

    1. 如果一個Service類沒有介面,又使用事務,需要生成代理,預設使用jdbc proxy。由於這個類沒有interface,就會丟擲異常。

關於異常

@Transaction預設的回滾是RunTimeException,使用JdbcTemplate或者Mybatis產生的都是RunTimeException。如果是下面這種程式碼:

Code7

    @Transactional
    public void transactionInJdbcApi2() throws Exception{
        Connection connection = DataSourceUtils.getConnection(dataSource);

        PreparedStatement preparedStatement = connection.prepareStatement("INSERT  INTO foo(id, name) values(1, 'aaa')");
        preparedStatement.executeUpdate();

        PreparedStatement preparedStatement2 = connection.prepareStatement("INSERT  INTO foo(id, name) values(2, 'xxx')");
        preparedStatement2.executeUpdate();
    }

Code7程式碼是不能支援事務的,如果在第二個插入語句出現主鍵重複異常時,丟擲的是check exception,不能回滾。
為了解決上面的問題,我們讓配置的回滾規則和出現的異常對應起來就行了。比如,1:配置@Transaction針對所有異常進行回滾。2.對Exception進行處理,使其丟擲RunTimeException。

Using the PlatformTransactionManager

You can also use the org.springframework.transaction.PlatformTransactionManager directly to manage your transaction. Simply pass the implementation of the PlatformTransactionManager you are using to your bean through a bean reference. Then, using the TransactionDefinition and TransactionStatus objects you can initiate transactions, roll back, and commit.

    DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    // explicitly setting the transaction name is something that can only be done programmatically
    def.setName("SomeTxName");
    def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    TransactionStatus status = txManager.getTransaction(def);
    try {
        // execute your business logic here
    }
    catch (MyException ex) {
        txManager.rollback(status);
        throw ex;
    }
    txManager.commit(status);

相關推薦

mybatis原始碼解析()-配合spring-tx實現事務原理

spring-tx關鍵物件 物件包含關係 TransactionAspectSupport.TransactionInfo包含TransactionStatus物件,TransactionStatus物件包含DataSourceTransacti

ReactiveSwift原始碼解析() SignalProtocol的take(first)與collect()延展實現

上篇部落格我們聊了observe()、map()、filter()延展函式的具體實現方式以及使用方式。我們在之前的部落格中已經聊過,Signal的主要功能是位於SignalProtocol的協議延展中的,而且延展函式是非常的多的。今天部落格中我們繼續來聊SignalProtocol中那些比較核心的延展實現。本

mybatis原始碼解析(五)-mybatis如何實現事務控制

這篇部落格主要展示mybatis是如何控制事務的。 下面兩個例子分別展示一下我們使用/不使用Trancation的例子。 Code 1不使用事務,自動提交 SqlSession session = sqlSessionFactory.o

Mybatis 原始碼解析二、Mapper介面的代理實現過程 MapperScannerConfigurer 解析

一、使用包掃描的配置方式生成Mapper的動態代理物件 上一篇文章中介紹了SqlSessionFactoryBean 通過Mybatis主配置檔案生成Mapper介面的代理物件,並將其儲存到Configuration中,但是一般在開發中我們並不是採用這種方式配置Ma

Spring Security原始碼分析Spring Social社交登入原始碼解析

在Spring Security原始碼分析三:Spring Social實現QQ社交登入和Spring Security原始碼分析四:Spring Social實現微信社交登入這兩章中,我們使用Spring Social已經實現了國內最常用的QQ和微信社交

Spring Boot系列(三):Spring Boot整合Mybatis原始碼解析

一、Mybatis回顧   1、MyBatis介紹   Mybatis是一個半ORM框架,它使用簡單的 XML 或註解用於配置和原始對映,將介面和Java的POJOs(普通的Java 物件)對映成資料庫中的記錄。   2、Mybatis整體架構  二、Spring Boot整合Mybatis +

myBatis原始碼解析-二級快取的實現方式

1. 前言 前面近一個月去寫自己的mybatis框架了,對mybatis原始碼分析止步不前,此文繼續前面的文章。開始分析mybatis一,二級快取的實現。附上自己的專案github地址:https://github.com/xbcrh/simple-ibatis  對mybatis感興趣的同學可關注

企業分布式微服務雲SpringCloud SpringBoot mybatis (十Spring Boot中使用LDAP來統一管理用戶信息

數據庫表 repo on() intellij attr ads get 可選 mail LDAP簡介 LDAP(輕量級目錄訪問協議,Lightweight Directory Access Protocol)是實現提供被稱為目錄服務的信息服務。目錄服務是一種特殊的數據庫系

Elastic-Job原始碼解析(二)之定時核心實現quartz

Elastic-Job是一個分散式定時任務框架,其內部的定時主要是利用quartz來實現,而Elastic-Job核心是對quartz進行了封裝,並提供了分散式任務的功能。具體是怎麼實現呢? 怎麼實現分散式呢? 主要是通過Zookeeper通訊,獲取任務伺服器ip地址,並通

MyBatis原始碼解析之日誌記錄

一 .概述 MyBatis沒有提供日誌的實現類,需要接入第三方的日誌元件,但第三方日誌元件都有各自的Log級別,且各不相同,但MyBatis統一提供了trace、debug、warn、error四個級別; 自動掃描日誌實現,並且第三方日誌外掛載入優先順序如下:slf4J → commonsLoging →

【學習轉載】MyBatis原始碼解析——日誌記錄

宣告:轉載自前輩:開心的魚a1 一 .概述 MyBatis沒有提供日誌的實現類,需要接入第三方的日誌元件,但第三方日誌元件都有各自的Log級別,且各不相同,但MyBatis統一提供了trace、debug、warn、error四個級別; 自動掃描日誌實現,並且第三方日誌外掛載入優先順序如下:sl

MyBatis原始碼解析之資料來源(含資料庫連線池簡析)

一.概述: 常見的資料來源元件都實現了javax.sql.DataSource介面; MyBatis不但要能整合第三方的資料來源元件,自身也提供了資料來源的實現; 一般情況下,資料來源的初始化過程引數較多,比較複雜; 二.設計模式: 為什麼要使用工廠模式     資料來

Colly原始碼解析——結合例子分析底層實現

        通過《Colly原始碼解析——主體流程》分析,我們可以知道Colly執行的主要流程。本文將結合http://go-colly.org上的例子分析一些高階設定的底層實現。(轉載請指明出於breaksoftware的csdn部落格) 遞迴深

Vue原始碼解析之陣列變異的實現

眾所周知,在 Vue 中,直接修改物件屬性的值無法觸發響應式。當你直接修改了物件屬性的值,你會發現,只有資料改了,但是頁面內容並沒有改變。 這是什麼原因? 原因在於: Vue 的響應式系統是基於Object.defineProperty這個方法的,該方法可以監聽物件中某個元素的獲取或修改,經過了該方法處理

Mybatis原始碼解析Mybatis初始化過程

一、搭建一個簡單的Mybatis工程 為了瞭解Mybatis的初始化過程,這裡需要搭建一個簡單的Mybatis工程操作資料庫,工程結構如下: 一個UserBean.java private int id; private String user

mybatis原始碼-解析配置檔案(三)之配置檔案Configuration解析(超詳細, 值得收藏)

1. 簡介 1.1 系列內容 本系列文章講解的是mybatis解析配置檔案內部的邏輯, 即 Reader reader = Resources.getResourceAsReader("mybatis-config.xml"); SqlSessionFact

mybatis原始碼-解析配置檔案(二)之解析的流程

1. 簡介 在之前的文章《mybatis 初步使用(IDEA的Maven專案, 超詳細)》中, 講解了mybatis的初步使用, 並總結了以下mybatis的執行流程: 通過 Resources 工具類讀取 mybatis-config.xml,

Spring4原始碼解析:BeanDefinition架構及實現

一、架構圖 首先共同看下總體的 Java Class Diagrams 圖: 二、具體類實現 2.1 AttributeAccessor 介面定義了一個通用的可對任意物件獲取、修改等操作元資料的附加契約。主要方法如下: public interface AttributeAcce

mybatis原始碼-解析配置檔案(四)之配置檔案Mapper解析

其中, mappers作為configuration節點的一部分配置, 在本文章中, 我們講解解析mappers節點, 即 xxxMapper.xml 檔案的解析。 1 解析入口 在解析 mybatis-config.xml 時, 會進行解析 xxxMapper.xml 的檔案。 在圖示流程的 XMLCo

mybatis原始碼-解析配置檔案(四-1)之配置檔案Mapper解析(cache)

1. 簡介 本文章主要講解的是, xxxMapper.xml 檔案中, cache 節點的原始碼。 2. 解析 XMLMapperBuilder.cacheElement() 方法主要負責解析 <cache> private void cacheElement(XNode context)