一文搞定 Spring事務

2022-09-30 06:00:40

Spring 事務

上文 使用SpringJDBC

1、JDBC事務控制

​ 不管你現在使用的是那一種ORM開發框架,只要你的核心是JDBC,那麼所有的事務處理都是圍繞著JDBC開展的,而JDBC之中的事務控制是由Connection介面提供的方法:

  • 1、關閉自動事務提交:connection.setAutoCommit(false);
  • 2、事務手工提交: connection.commit();
  • 3、事務回滾: connection.rollback();

在程式的開發之中事務的使用是存在有前提的:如果某一個業務現在需要同時執行若干條資料更新處理操作,這個時候才會使用到事務控制,除此之外是不需要強制性處理的。

按照傳統的事務控制處理方法來講一般都是在業務層進行處理的,而在之前分析過了如何基於AOP 設計思想採用動態代理設計模式實現的事務處理模型,這種操作可以在不侵入業務程式碼的情況下進行事務的控制,但是程式碼的實現過程實在是繁瑣,現在既然都有了AOP處理模型了,所以對於事務的控制就必須有一個完整的加強。

1.1、ACID事務原則

ACID主要指的是事務的四種特點:原子性(Atomicity)、一致性(Consistency)、隔離性或獨立性(lsolation)、永續性(Durabilily)四個特徵:

  • 原子性(Atomicity):整個事務中的所有操作,要麼全部完成,要麼全部不完成,不可能停滯在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣;
  • 一致性(Consistency):一個事務可以封裝狀態改變(除非它是一個唯讀的)。事務必須始終保持系統處於一致的狀態,不管在任何給定的時間並行事務有多少;
  • 隔離性(lsolation):隔離狀態執行事務,使它們好像是系統在給定時間內執行的唯一操作。如果有兩個事務,執行在相同的時間內,執行相同的功能,事務的隔離性將確保每一事務在系統中認為只有該事務在使用系統;
  • 永續性(Durability):在事務完成以後,該事務對資料庫所作的更改便持久的儲存在資料庫之中,並不會被回滾

2、Spring事務架構

Spring事務是對已有JDBC事務的進一步的包裝型處理,所以底層依然是JDBC事務控制,而後在這之上進行了更加合理的二次開發與設計,首先先來看一下Spring 與JDBC事務之間的結構圖。

​ 只要是說到了事務的開發,那麼就必須考慮到ORM元件的整合問題各類的ORM開發元件實在是太多了,同時Spring在設計的時候無法預知未來,那麼這個時候在Spring 框架裡面就針對於事務的接入提供了一個開發標準

​ Spring事務的核心實現關鍵是在於:PlatformTransactionManager

通過以上的程式碼可以發現,PlatfrmTransactionManager介面存在有一個TransactionManager父介面,下面開啟該介面的定義來觀察其具體功能。

​ 在現代的開發過程之中,最為核心的事務介面主要使用的是PlatformTransactionManager(這也就是長久以來的習慣),在Spring最早出現宣告式事務的時候,就有了這個處理介面了。在進行獲取事務的時候可以發現getTransaction()方法內部需要接收有一個TransactionDefinition介面範例,這個介面主要定義了Spring事務的超時時間,以及Spring事務的傳播屬性(是面試的關鍵所在),而在getTransaction()方法內部會返回有一個TransactionStatus介面範例,開啟這個介面來觀察一下。.

public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
	boolean hasSavepoint(); // 是否存在hasSavepoint (事務儲存點)
	void flush(); // 事務重新整理
}

而後該介面內部定義的時候又需要繼承TransactionExecution、SavepointManager(事務儲存點管理器)、Flushable(事務重新整理)三個父介面。下圖就是Spring事務的整體架構。

3、程式設計式事務控制

由於現在很少用到這種程式設計式事務了,導致很多初學者根本不知道這其中是怎麼設定的。其實萬變不離其宗,都是基於JDBC的事務控制。

  • 使用步驟
    • 設定事務-》 是資料來源
    • 編寫程式碼 控制事務

3.1如何使用

資料來源使用的是文章開始前的SpringJDBC的環境 地址

1 設定事務

public class TransactionConfig {
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        // PlatformTransactionManager 類似於一個事務定義的標準
        // DataSource 也是一個標準 規範資料來源
        DataSourceTransactionManager transactionManager =
                new DataSourceTransactionManager(dataSource); 
        // transactionManager.setDataSource(dataSource); 二選一即可
        return transactionManager;
    }
}

面試題: PlatformTransactionManager 與 TransactionManager兩者區別?

​ TransactionManager是後爹,是屬於PlatformTransactionManager父介面,但是現在不要輕易使用,因為很多的傳統的Spring開發專案還是使用的是PlatformTransactionManager。TransactionManager是為響應式程式設計做的準備。

2 編寫程式碼

    @Test
    public void testInsert()  {
        String sql = "insert into yootk.book(title,author,price) values(?,?,?)";
        LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, "Python入門", "李老師", 99.90));
        LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, "Java入門", null, 99.90));
        LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, "Js入門", "李老師", null));
    }

執行程式碼後會發現,出現異常提示資訊。由於我們的表中設定了not null,所以在插入是會出現異常。這也就是我們異常資訊的來源。

  • 由於出現了異常,可是,資料還是插入到資料庫,在正常開發中是不允許這樣的情況發現的,那麼該如何解決呢。還記得上面設定的事務資訊嗎。修改測試類如下:

    @Test
    public void testInsert() {
        String sql = "insert into yootk.book(title,author,price) values(?,?,?)";
        TransactionStatus status = transactionManager.getTransaction( //開啟事務
                new DefaultTransactionAttribute()); // 預設事務屬性
        try {
            LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, "Python入門", "李老師", 99.90));
            LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, "Java入門", null, 99.90));
            LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, "Js入門", "李老師", null));
            transactionManager.commit(status); // 提交
        } catch (DataAccessException e) {
            transactionManager.rollback(status); // 回滾
            throw new RuntimeException(e);
        }
    }

注意:執行先,需要先將資料庫表清空,能更好的觀察執行結果。

  • 而後會發現,雖然我們的程式執行出現異常了,但資料庫沒有資料。
  • 說明我們設定的事務生效了,使其出現異常,回滾了。

3.2TransactionStatus

如果現在僅僅是使用了TransactionManager提交和回滾的處理方法,僅僅是Spring提供的事務處理的皮毛所在,而如果要想深入的理解事務處理的特點,那麼就需要分析其每一個核心的組成類,首先分析的就是TransactionStatus。

在開啟事務的時候會返回有一個TransactionStatus介面範例,而後在提交或回滾事務的時候都需要針對於指定的status範例進行處理,首先來開啟這個介面的定義關聯結構。

DefaultTransactionStatus是TransactionStatus預設實現的子類而後該類並不是直接範例化的,而是通過事務管理器負責範例化處理的,status所得到的是一個事務的處理標記,而後Spring依照此標記管理事務。

現我們有以下業務,在業務執行過程中,有一部分業務執行失敗,正常來說,是執行回滾操作,但是現在我們要讓某一個位置之前的執行的sql不回滾。那麼這個功能如何實現呢?

這裡就需要用到我們事務的儲存點:

    @Test
    public void testInsertSavePoint() { // 測試事務的儲存點
        String sql = "insert into yootk.book(title,author,price) values(?,?,?)";
        TransactionStatus status = transactionManager.getTransaction( // 開啟事務
                new DefaultTransactionAttribute()); // 預設事務屬性
        Object savepointA = null; //儲存點
        try {
            LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, "Python入門", "李老師", 99.90));
            savepointA = status.createSavepoint(); // 建立儲存點
            LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, "Java入門", null, 99.90));
            transactionManager.commit(status); // 正常執行 事務提交
        } catch (DataAccessException e) {
            // 出現異常 先回滾到儲存點 然後在提交儲存點之前的事務
            status.releaseSavepoint(savepointA);  // 回滾到儲存點
            transactionManager.commit(status); // 提交
            throw new RuntimeException(e);
        }
    }

4、Spring事務隔離級別

Spring面試之中隔離級別的面試問題是最為常見的,也是一個核心的基礎所在,但是所謂的隔離級別一定要記住,是在並行環境存取下才會存在的問題。資料庫是一個專案應用中的公共儲存資源,所以在實際的專案開發過程中,很有可能會有兩個不同的執行緒(每個執行緒擁有各自的資料庫事務),要進行同一條資料的讀取以及更新操作。

下面就通過程式碼的形式 一步步的揭開他的廬山真面目。

  • 對於事務,
    private class BookRowMapper implements RowMapper<Book> {  // 物件對映關係
        @Override
        public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
            Book book = new Book();
            book.setBid(rs.getInt(1));
            book.setTitle(rs.getString(2));
            book.setAuthor(rs.getString(3));
            book.setPrice(rs.getDouble(4));
            return book;
        }
    }
    @Test
    public void testInsertIsolation() throws InterruptedException { // 測試事務的隔離級別
        String query = "select bid,title,author,price from yootk.book where bid = ?"; // 查詢
        String update = "update yootk.book set title = ?, author =? where bid =?"; // 根據id修改
        BookRowMapper bookRowMapper = new BookRowMapper(); // 對Book物件的對映
        DefaultTransactionDefinition definition =
                new DefaultTransactionDefinition(); // 建立預設事務物件
        Thread threadA = new Thread(() -> {
            TransactionStatus statusA = this.transactionManager.getTransaction(definition); //開始事務
            Book book = this.jdbcTemplate.queryForObject(query, bookRowMapper, 1); // 查詢bid = 1的資料
            String name = Thread.currentThread().getName();// 獲取執行緒名稱
            System.out.println(11111 + "??????");
            LOGGER.info("{}【查詢結果】:{}", name, book);
            try {
                TimeUnit.SECONDS.sleep(2); //等待兩秒 讓執行緒B修改之後再查詢
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            book = jdbcTemplate.queryForObject(query, bookRowMapper, 1); // 再次查詢
            LOGGER.info("{}【查詢結果】:{}", name, book);
        }, "事務執行緒-A");
        Thread threadB = new Thread(() -> {
            TransactionStatus statusB =
                    transactionManager.getTransaction(definition); // 開啟事務
            String name = Thread.currentThread().getName();// 獲取執行緒名稱
            int i = 0;
            try {
                i = jdbcTemplate.update(update, "Netty", "李老師", 1);
                LOGGER.info("{} 執行結果:{}", name, i);
                transactionManager.commit(statusB); // 提交事務
            } catch (DataAccessException e) {
                transactionManager.rollback(statusB); // 回滾事務
                throw new RuntimeException(e);
            }
        }, "事務執行緒-B");
        threadB.start();// 啟動執行緒
        threadA.start();

        threadA.join();// 等待相互執行完成
        threadB.join();
    }

執行結果

事務執行緒-A【查詢結果】:Book(bid=1, title=Netty, author=李老師, price=99.9)
事務執行緒-B 執行結果:1  
事務執行緒-A【查詢結果】:Book(bid=1, title=Netty, author=李老師, price=99.9)

檢視執行結果可知,我們執行緒B執行的是更新操作,但是更新成功後,在事務A進行查詢時,本應是我們更新後的資料,這才對呀。所以這個事務出現了事務不同步的問題。

為了保證並行狀態下的資料讀取的正確性,就需要通過事務的隔離級別來進行控制,實際上控制的就是髒讀、幻讀以及不可重複讀的問題了。

4.1、髒讀

髒讀(Dirty reads):事務A在讀取資料時,讀取到了事務B未提交的資料,由於事務B有可能被回滾,所以該資料有可能是一個無效資料

4.2、不可重複讀

不可重複讀(Non-repeatable Reads):事務A對一個資料的兩次讀取返回了不同的資料內容,有可能在兩次讀取之間事務B對該資料進行了修改,一般此類操作出現在資料修改操作之中;

4.3、幻讀

幻讀(Phantom Reads):事務A在進行資料兩次查詢時產生了不一致的結果,有可能是事務B在事務A第二次查詢之前增加或刪除了資料內容所造成的.

Spring最大的優勢是在於將所有的設定過程都進行了標準化的定義,於是在TransactionDefintion介面裡面就提供了資料庫隔離級別的定義常數。

從正常的設計角度來講,在進行Spring事務控制的時候,不要輕易的去隨意修改隔離級別(需要記住這幾個隔離級別的概念),因為一般都使用預設的隔離級別,由資料庫自己來實現的控制。

【MySQL資料庫】檢視MySQL資料庫之中的預設隔離級別

SHOW VARIABLES LIKE 'transaction_isolation';

舉個栗子,來看看隔離級別的作用吧

修改testInsertIsolation測試類

definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);// 設定事務隔離級別為:讀已提交

執行結果:

因為我們執行緒B在修改後,就提交了,而我們設定的隔離級別是讀已提交,所以能讀到已提交的資料

事務執行緒-A【查詢結果】:Book(bid=1, title=Java入門到入土, author=李老師, price=99.9) 
事務執行緒-B 執行結果:1  
事務執行緒-A【查詢結果】:Book(bid=1, title=Netty, author=李老師, price=99.9)  

5、Spring事務傳播機制

事務開發是和業務層有直接聯絡的,在進行開發的過程之中,很難出現業務層之間不互相呼叫的場景,例如:存在有一個A業務處理,但是A業務在處理的時候有可能會呼叫B業務,那麼如果此時A和B之間各自都存在有事務的機制,那麼這個時候就需要進行事務有效的傳播管理。

1、TransactionDefinition.PROPAGATION_REQUIRED:預設事務隔離級別,子業務直接支援當前父級事務,如果當前父業務之中沒有事務,則建立一個新的事務,如果當前父業務之中存在有事務,則合併為一個完整的事務。簡化的理解:不管任何的時候,只要進行了業務的呼叫,都需要建立出一個新的事務,這種機制是最為常用的事務傳播機制的設定。

2、TransactionDefinition.PROPAGATION_SUPPORTS:如果當前父業務存事務,則加入該父級事務。如果當前不存在有父級事務,則以非事務方式執行;

3、TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事務的方式執行,如果當前存在有父級事務,則先自動掛起父級事務後執行;

4、TransactionDefinition.PROPAGATION_MANDATORY:如果當前存在父級事務,則執行在父級事務之中,如果當前無事務則丟擲異常(必須存在有父級事務);

5、TransactionDefinition.PROPAGATION_REQUIRES_NEW:建立一個新的子業務事務,如果存在有父級事務則會自動將其掛起,該操作可以實現子事務的獨立提交,不受呼叫者的事務影響,即便父級事務異常,也可以正常提交;

6、TransactionDefinition.PROPAGATION_NEVER:以非事務的方式執行,如果當前存在有事務則丟擲異常;

7、TransactionDefinition.PROPAGATION_NESTED:如果當前存在父級事務,則當前子業務中的事務會自動成為該父級事務中的一個子事務,只有在父級事務提交後才會提交子事務。如果子事務產生異常則可以交由父級呼叫進行例外處理,如果父級事務產生異常,則其也會回滾。