1. 程式人生 > >基於可靠訊息方案的分散式事務(二):Java中的事務

基於可靠訊息方案的分散式事務(二):Java中的事務

前言:在上一篇文章 基於可靠訊息方案的分散式事務:Lottor介紹 中介紹了常見的分散式事務的解決方案以及筆者基於可靠訊息方案實現的分散式事務元件Lottor的原理,並展示了應用的控制檯管理。在正式介紹Lottor的具體實現之前,本文首先將會介紹Java中的事務管理,具體來說是Spring的事務管理。PS:有很多讀者提問Lottor是否開源,這裡統一回答:是開源的,Lottor目前在筆者所在公司的內部專案應用,並且筆者在將耦合的業務程式碼重構,將會在下一篇文章同步更新到GitHub,敬請期待。本文較長,適合電腦閱讀。如果已對Java中的事務瞭解,可略過本文,歡迎關注本系列文章。

Java 事務的型別

事務管理是應用系統開發中必不可少的一部分。Java事務的型別有三種:JDBC事務、JTA(Java Transaction API)事務、容器事務。 常見的容器事務如Spring事務,容器事務主要是J2EE應用伺服器提供的,容器事務大多是基於JTA完成,這是一個基於JNDI的,相當複雜的API實現。首先介紹J2EE開發中的兩個事務:JDBC事務和JTA事務。

JDBC 事務

JDBC的一切行為包括事務是基於一個Connection的,在JDBC中是通過Connection物件進行事務管理。在JDBC中,常用的和事務相關的方法是: setAutoCommit、commit、rollback等。

預設情況下,當我們建立一個數據庫連線時,會執行在自動提交模式(Auto-commit)下。這意味著,任何時候我們執行一條SQL完成之後,事務都會自動提交。所以我們執行的每一條SQL都是一個事務,並且如果正在執行DML或者DDL語句,這些改變會在每一條SQL語句結束的時存入資料庫。有時候我們想讓一組SQL語句成為事務的一部分,那樣我們就可以在所有語句執行成功的時候提交,並且如果出現任何異常,這些語句作為事務的一部分,我們可以選擇將其全部回滾。

public static void main(String[] args) {
        Connection con = null;
        try
{ con = DBConnection.getConnection(); con.setAutoCommit(false); insertAccountData(con, 1, "Pankaj"); insertAddressData(con, 1, "Albany Dr", "San Jose", "USA"); } catch (SQLException | ClassNotFoundException e) { e.printStackTrace(); try { con.rollback(); System.out.println("JDBC Transaction rolled back successfully"); } catch (SQLException e1) { System.out.println("SQLException in rollback" + e.getMessage()); } } finally { try { if (con != null) con.close(); } catch (SQLException e) { e.printStackTrace(); } } }

上面的程式碼實現了一個簡單的插入賬號和地址資訊的功能,通過事務來控制插入操作,要麼都提交,要麼都回滾。

JDBC為使用Java進行資料庫的事務操作提供了最基本的支援。通過JDBC事務,我們可以將多個SQL語句放到同一個事務中,保證一個 JDBC 事務不能跨越多個數據庫!所以,如果涉及到多資料庫的操作或者分散式場景,JDBC事務就無能為力了。

JTA 事務

通常,JDBC事務就可以解決資料的一致性等問題,鑑於他用法相對簡單,所以很多人關於Java中的事務只知道有JDBC事務,或者有人知道框架中的事務(比如Hibernate、Spring)等。但是,由於JDBC無法實現分散式事務,而如今的分散式場景越來越多,所以,JTA事務就應運而生。

JTA(Java Transaction API),Java事務API允許應用程式執行分散式事務,也就是說事務可以訪問或更新兩個或更多網路上的計算機資源。JTA指定事務管理器和分散式事務系統中涉及的各方之間的標準Java介面:應用程式,應用程式伺服器和控制對受事務影響的共享資源的訪問的資源管理器。一個事務定義了完全成功或根本不產生結果的邏輯工作單元。

Java 事務程式設計介面(JTA:Java Transaction API)和 Java 事務服務 (JTS;Java Transaction Service) 為 J2EE 平臺提供了分散式事務服務。分散式事務(Distributed Transaction)包括事務管理器(Transaction Manager)和一個或多個支援 XA 協議的資源管理器 ( Resource Manager )。我們可以將資源管理器看做任意型別的持久化資料儲存;事務管理器承擔著所有事務參與單元的協調與控制。JTA 事務有效的遮蔽了底層事務資源,使應用可以以透明的方式參入到事務處理中;但是與本地事務相比,XA 協議的系統開銷大,在系統開發過程中應慎重考慮是否確實需要分散式事務。若確實需要分散式事務以協調多個事務資源,則應實現和配置所支援 XA 協議的事務資源,如 JMS、JDBC 資料庫連線池等。

JTA裡面提供了 java.transaction.UserTransaction ,裡面定義了下面幾個方法:

  • begin()- 開始一個分散式事務,(在後臺 TransactionManager 會建立一個 Transaction 事務物件並把此物件通過 ThreadLocale 關聯到當前執行緒上 )
  • commit()- 提交事務(在後臺 TransactionManager 會從當前執行緒下取出事務物件並把此物件所代表的事務提交)
  • rollback()- 回滾事務(在後臺 TransactionManager 會從當前執行緒下取出事務物件並把此物件所代表的事務回滾)
  • getStatus()- 返回關聯到當前執行緒的分散式事務的狀態 (Status 物件裡邊定義了所有的事務狀態)
  • setRollbackOnly()- 標識關聯到當前執行緒的分散式事務將被回滾

值得注意的是,不是使用了UserTransaction就能把普通的JDBC操作直接轉成JTA操作,JTA對DataSource、Connection和Resource 都是有要求的,只有符合XA規範,並且實現了XA規範的相關介面的類才能參與到JTA事務中來。

關於XA規範,讀者可以根據前一篇文章進行補充,目前主流的資料庫都支援XA規範。使用的流程如下:

要想使用用 JTA 事務,那麼就需要有一個實現 javax.sql.XADataSource 、 javax.sql.XAConnection 和 javax.sql.XAResource 介面的 JDBC 驅動程式。一個實現了這些介面的驅動程式將可以參與 JTA 事務。一個 XADataSource 物件就是一個 XAConnection 物件的工廠。XAConnection 是參與 JTA 事務的 JDBC 連線。

要使用JTA事務,必須使用XADataSource來產生資料庫連線,產生的連線為一個XA連線。

XA連線(javax.sql.XAConnection)和非XA(java.sql.Connection)連線的區別在於:XA可以參與JTA的事務,而且不支援自動提交。

除了資料庫,還有JBoss、JMS的訊息中介軟體ActiveMQ等很多元件都是遵守XA協議,2PC和3PC也是符合XA規範的。具體JTA更多的內容,本文不再展開,後面有機會專門深入介紹JTA事務。

Spring 事務管理

Spring支援程式設計式事務管理和宣告式事務管理兩種方式。Spring所有的事務管理策略類都繼承自PlatformTransactionManager介面,類圖如下:

PlatformTransactionManager類圖

public interface PlatformTransactionManager {
    TransactionStatus getTransaction(TransactionDefinition var1) throws TransactionException;

    void commit(TransactionStatus var1) throws TransactionException;

    void rollback(TransactionStatus var1) throws TransactionException;
}

其中,#getTransaction(TransactionDefinition)方法根據指定的傳播行為返回當前活動的事務或建立一個新的事務,這個方法裡面的引數是TransactionDefinition類,這個類定義了一些基本的事務屬性。 其他兩個方法,分別是提交和回滾,傳入TransactionStatus事務狀態值。

事務屬性

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";
    // 定義事務的傳播規則
    Propagation propagation() default Propagation.REQUIRED;
    // 指定事務的隔離級別
    Isolation isolation() default Isolation.DEFAULT;
    // 對於長時間執行的事務定義超時時間
    int timeout() default -1;
    // 指定事務為只讀
    boolean readOnly() default false;
    // rollback-for指定事務對哪些檢查型異常應當回滾而不提交
    Class<? extends Throwable>[] rollbackFor() default {};
    // 導致事務回滾的異常類名字陣列
    String[] rollbackForClassName() default {};
    // no-rollback-for指定事務對哪些異常應當繼續執行而不回滾
    Class<? extends Throwable>[] noRollbackFor() default {};
    // 不會導致事務回滾的異常類名字陣列
    String[] noRollbackForClassName() default {};
}

其實通過 @Transactional可以瞭解事務屬性的定義。事務屬性描述了事務策略如何應用到方法上,事務屬性包含5個方面:

  • 傳播行為
  • 隔離級別
  • 回滾規則
  • 事務超時
  • 是否只讀

Spring 事務傳播屬性

傳播行為定義了客戶端與被呼叫方法之間的事務邊界,即傳播規則回答了這樣的一個問題,新的事務應該被啟動還是掛起,或者方法是否要在事務環境中執行。Spring定義了7個以PROPAGATION_開頭的常量表示它的傳播屬性。在TransactionDefinition中定義瞭如下的常量:

名稱 解釋
PROPAGATION_REQUIRED 0 支援當前事務,如果當前沒有事務,就新建一個事務。這是最常見的選擇,也是Spring預設的事務的傳播。
PROPAGATION_SUPPORTS 1 支援當前事務,如果當前沒有事務,就以非事務方式執行。
PROPAGATION_MANDATORY 2 支援當前事務,如果當前沒有事務,就丟擲異常。
PROPAGATION_REQUIRES_NEW 3 新建事務,如果當前存在事務,把當前事務掛起。
PROPAGATION_NOT_SUPPORTED 4 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
PROPAGATION_NEVER 5 以非事務方式執行,如果當前存在事務,則丟擲異常。
PROPAGATION_NESTED 6 如果當前存在事務,則在巢狀事務內執行。如果當前沒有事務,則進行與PROPAGATION_REQUIRED類似的操作。

Spring 隔離級別

隔離級別定義了一個事務可能受其他併發事務影響的程度。多事務併發可能會導致髒讀、幻讀、不可重複讀等各種讀現象。在TransactionDefinition中定義瞭如下的常量:

水果 價格 數量
ISOLATION_DEFAULT -1 這是一個PlatfromTransactionManager預設的隔離級別,使用資料庫預設的事務隔離級別。另外四個與JDBC的隔離級別相對應
ISOLATION_READ_UNCOMMITTED 1 這是事務最低的隔離級別,它充許另外一個事務可以看到這個事務未提交的資料。這種隔離級別會產生髒讀,不可重複讀和幻讀。
ISOLATION_READ_COMMITTED 2 保證一個事務修改的資料提交後才能被另外一個事務讀取。另外一個事務不能讀取該事務未提交的資料。
ISOLATION_REPEATABLE_READ 4 這種事務隔離級別可以防止髒讀,不可重複讀。但是可能出現幻讀。
ISOLATION_SERIALIZABLE 8 這是花費最高代價但是最可靠的事務隔離級別。事務被處理為順序執行。除了防止髒讀,不可重複讀外,還避免了幻讀。

是否只讀

如果事務只對後端的資料庫進行讀操作,資料庫可以利用事務ID只讀特性來進行一些特定的優化。通過將事務設定為只讀,你就可以給資料庫一個機會,讓他應用它認為合適的優化措施。因為是否只讀是在事務啟動的時候由資料庫實施的,所以只有對那些具備可能啟動一個新事務的傳播行為(PROPAGATION_REQUIRED,PROPAGATION_REQUIRED_NEW,PROPAGATION_NESTED)的方法來說,才有意義。

事務超時

為了使應用程式很好地執行,事務不能執行太長時間。因為超時時鐘會在事務開始時啟動,所以只有對那些具備可能啟動一個新事務的傳播行為(同上幾種)的方法來說,才有意義。預設設定為底層事務系統的超時值,如果底層資料庫事務系統沒有設定超時值,那麼就是none,沒有超時限制。

事務回滾

事務回滾規則定義了哪些異常會導致事務回滾而哪些不會。預設情況下,事務只有在遇到執行時期異常才回滾,而在遇到檢查型異常時不會回滾。就是丟擲的異常為RuntimeException的子類(Errors也會導致事務回滾),而丟擲checked異常則不會導致事務回滾。可以明確的配置在丟擲那些異常時回滾事務,包括checked異常。也可以明確定義那些異常丟擲時不回滾事務。還可以通過程式設計的setRollbackOnly()方法來指示一個事務必須回滾,在呼叫完setRollbackOnly()後所能執行的唯一操作就是回滾。

配置 Spring 事務管理器

Spring中使用xml進行如下的配置即可:

 <!-- 配置事務管理器 -->  
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
        <property name="dataSource" ref="dataSource" />  
    </bean>  

如果是Spring Boot,我們只需要設定好DataSource即可,DataSourceTransactionManagerAutoConfiguration會自動配置好DataSourceTransactionManager

        @Bean
        @ConditionalOnMissingBean(PlatformTransactionManager.class)
        public DataSourceTransactionManager transactionManager(
                DataSourceProperties properties) {
            DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(
                    this.dataSource);
            if (this.transactionManagerCustomizers != null) {
                this.transactionManagerCustomizers.customize(transactionManager);
            }
            return transactionManager;
        }

配置了事務管理器後,事務當然還是得我們自己去操作,Spring提供了兩種事務管理的方式:程式設計式事務管理和宣告式事務管理,下面分別看一下如何應用。

程式設計式事務管理

程式設計式事務管理我們可以通過PlatformTransactionManager實現來進行事務管理,同樣的Spring也為我們提供了模板類TransactionTemplate進行事務管理,下面主要介紹模板類。

模板類

我們需要在配置檔案中配置:

    <!--配置事務管理的模板-->
    <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
        <property name="transactionManager" ref="transactionManager"></property>
        <!--定義事務隔離級別,-1表示使用資料庫預設級別-->
        <property name="isolationLevelName" value="ISOLATION_DEFAULT"></property>
        <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"></property>
    </bean>

TransactionTemplate幫我們封裝了許多程式碼,節省了我們的工作。專門建了一張account表,用於模擬存錢的一個場景。

    //BaseSeviceImpl.java
    @Override
    public void insert(String sql, boolean flag) throws Exception {
        dao.insertSql(sql);
        // 如果flag 為 true ,丟擲異常
        if (flag) {
            throw new Exception("has exception!!!");
        }
    }
    //獲取總金額
    @Override
    public Integer sum() {
        return dao.sum();
    }
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring-test.xml"})
public class TransactionTest{
    @Resource
    private TransactionTemplate transactionTemplate;
    @Autowired
    private BaseSevice baseSevice;

    @Test
    public void transTest() {
        System.out.println("before transaction");
        Integer sum1 = baseSevice.sum();
        System.out.println("before transaction sum: "+sum1);
        System.out.println("transaction....");
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                try {
                    baseSevice.insert("INSERT INTO account VALUES (100);",false);
                    baseSevice.insert("INSERT INTO account VALUES (100);",false);
                } catch (Exception e) {
                    status.setRollbackOnly();
                    e.printStackTrace();
                }
           }
        });
        System.out.println("after transaction");
        Integer sum2 = baseSevice.sum();
        System.out.println("after transaction sum: "+sum2);
    }
}

對於丟擲Exception型別的異常且需要回滾時,需要捕獲異常並通過呼叫status物件的setRollbackOnly()方法告知事務管理器當前事務需要回滾。

宣告式事務管理

宣告式事務管理有兩種常用的方式,一種是基於tx和aop名稱空間的xml配置檔案,一種是基於@Transactional註解,隨著Spring和Java的版本越來越高,越趨向於使用註解的方式。

基於tx和aop名稱空間的xml配置檔案

<tx:advice id="advice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="insert" propagation="REQUIRED" read-only="false"  rollback-for="Exception"/>
        </tx:attributes>
    </tx:advice>

    <aop:config>
        <aop:pointcut id="pointCut" expression="execution (* com.blueskykong.service.*.*(..))"/>
        <aop:advisor advice-ref="advice" pointcut-ref="pointCut"/>
    </aop:config>
    @Test
    public void transTest() {
        System.out.println("before transaction");
        Integer sum1 = baseSevice.sum();
        System.out.println("before transaction sum: "+sum1);
        System.out.println("transaction....");
        try{
            baseSevice.insert("INSERT INTO account VALUES (100);",true);
        } catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("after transaction");
        Integer sum2 = baseSevice.sum();
        System.out.println("after transaction sum: "+sum2);
    }

基於@Transactional註解

最常用的方式,也是簡單的方式。只需要在配置檔案中開啟對註解事務管理的支援。

<tx:annotation-driven transaction-manager="transactionManager"/>

而Spring boot啟動類必須要開啟事務,而開啟事務用的註解就是@EnableTransactionManagement

    @Transactional(rollbackFor=Exception.class)
    public void insert(String sql, boolean flag) throws Exception {
        dao.insertSql(sql);
        // 如果flag 為 true ,丟擲異常
        if (flag){
            throw new Exception("has exception!!!");
        }
    }

指定出現Exception異常的時候回滾,遇到檢查性的異常需要回滾,預設情況下非檢查性異常,包括error也會自動回滾。

總結

本文主要介紹了Java事務的型別:JDBC事務、JTA(Java Transaction API)事務、容器事務。簡要介紹了JDBC事務和JTA事務,詳細介紹了Spring的事務管理,Spring對事務管理是通過事務管理器來實現的。Spring提供了許多內建事務管理器實現,如資料來源事務管理器DataSourceTransactionManager、整合JPA的事務管理JpaTransactionManager等。分別介紹了Spring事務屬性包含的5個方面,以及Spring提供的兩種事務管理的方式:程式設計式事務管理和宣告式事務管理。其中宣告式的事務管理,最為便捷、使用的更多。通過本文的介紹,希望讀者在接觸分散式事務時,首先對Java中的事務能夠熟悉。JTA事務時,其實也引出了分散式事務的相關概念,對應2PC和3PC的XA規範。

推薦閱讀

訂閱最新文章,歡迎關注我的公眾號

微信公眾號

參考