這篇文章以一個問題開始,如果你知道答案的話就可以跳過不看啦@(o・ェ・)@

Q:在一個批量任務執行的過程中,呼叫多個子任務時,如果有一些子任務發生異常,只是回滾那些出現異常的任務,而不是整個批量任務,請問在Spring中事務需要如何配置才能實現這一功能呢?

隔離級別

隔離性(Isolation)作為事務特性的一個關鍵特性,它要求每個讀寫事務的物件對其他事務的操作物件能相互分離,即該事務提交前對其他事務都不可見,在資料庫層面都是使用鎖來實現。

事務的隔離級別從低到高有以下四種:

  • READ UNCOMMITTED(未提交讀):這是最低的隔離級別,其含義是允許一個事務讀取另外一個事務沒有提交的資料。READ UNCOMMITTED是一種危險的隔離級別,在實際開發中基本不會使用,主要是由於它會帶來髒讀問題。

    時間 事務1 事務2 備註
    1 讀取庫存為100 --- ---
    2 扣減庫層50 --- 剩餘50
    3 扣減庫層50 ---
    4 提交事務 庫存儲存為0
    5 回滾事務 --- 事務回滾為0

髒讀對於要求資料一致性的應用來說是致命的,目前主流的資料庫的隔離級別都不會設定成READ UNCOMMITTED。不過髒讀雖然看起來毫無用處,但是它主要優點是併發能力高,適合那些對資料一致性沒有要求而追求高併發的場景。

  • READ COMMITTED(讀寫提交): 它是指一個事務只能讀取另外一個事務已經提交的資料,不能讀取未提交的資料。READ COMMITTED會帶來不可重複讀的問題:
時間 事務1 事務2 備註
1 讀取庫存為1
2 扣減庫存 事務未提交
3 讀取庫存為1
4 提交事務 庫存變成0
5 扣減庫存 庫存為0,無法扣減

不可重複讀和髒讀的區別是:髒讀讀取到的是未提交的資料,而不可重複讀讀到的確實已經提交的資料,但是違反了資料庫事務一致性的要求。

一般來說,不可重複讀的問題是可以接受的,因為其讀到的是已經提交的資料,本身並不會帶來很大的問題。因此,很多資料庫如(ORACLE,SQL SERVER)將其預設隔離級別設定為READ COMMITTED,允許不可重複讀的現象。

  • REPEATABLE READ (可重複讀):可重複讀的目標是為了克服READ COMMITED中出現的不可重複讀,它指在同一個事務內的查詢都是與事務開始時刻一致,以上表為例,在REPEATABLE READ隔離級別下它會發生如下變化:
時間 事務1 事務2 備註
1 讀取庫存為1
2 扣減庫存 事務未提交
3 讀取庫存 不允許讀取,等待事務1提交
4 提交事務 庫存變成0
5 讀取庫存 庫存為0,無法扣減

REPEATABLE READ雖然解決了不可重複讀問題,但是他又會帶來幻讀問題,幻讀是指,在一個事務中,第一次查詢某條記錄,發現沒有,但是,當試圖更新這條不存在的記錄時,竟然能成功,並且,再次讀取同一條記錄,它就神奇地出現了。

時間 事務A 事務2 備註
1 begin begin
2 讀取id為100的資料 沒有資料
3 插入id為100的資料
4 提交事務
5 讀取id為100的資料 沒有資料
6 更新id為100的資料 成功
7 讀取id為100的資料 讀取成功
8 提交事務

事務B在第2步第一次讀取id=99的記錄時,讀到的記錄為空,說明不存在id=99的記錄。隨後,事務A在第3步插入了一條id=99的記錄並提交。事務B在第5步再次讀取id=99的記錄時,讀到的記錄仍然為空,但是,事務B在第6步試圖更新這條不存在的記錄時,竟然成功了,並且,事務B在第8步再次讀取id=99的記錄時,記錄出現了。

  • SERIALIZABLE(序列化):資料庫最高的隔離級別,它要求所有的SQL都會按照順序執行,這樣可以克服上述所有隔離出現的各種問題,能夠完全包住資料的一致性。

Spring中配置隔離級別

在Spring專案中配置隔離級別只需要做如下操作

@Transactional(isolation = Isolation.SERIALIZABLE)
public int insertUser(User user){
    return userDao.insertUser(user);
}

上面的程式碼中我們使用了序列化的隔離級別來包住資料的一致性,這使它將阻塞其他的事務進行併發,所以它只能運用在那些低併發而又需要保證資料一致性的場景下。

隔離級別字典:

DEFAULT(-1),  ## 資料庫預設級別
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);

傳播行為

在Spring中,當一個方法呼叫另外一個方法時,可以讓事務採取不同的策略工作,如新建事務或者掛起當前事務等,這便是事務的傳播行為。

定義

在Spring的事務機制中對資料庫存在7種傳播行為,通過列舉類Propagation定義。

public enum Propagation {
    /**
     * 需要事務,預設傳播性行為。
     * 如果當前存在事務,就沿用當前事務,否則新建一個事務執行子方法
     */
    REQUIRED(0),
    /**
     * 支援事務,如果當前存在事務,就沿用當前事務,
     * 如果不存在,則繼續採用無事務的方式執行子方法
     */
    SUPPORTS(1),
    /**
     * 必須使用事務,如果當前沒有事務,丟擲異常
     * 如果存在當前事務,就沿用當前事務
     */
    MANDATORY(2),
    /**
     * 無論當前事務是否存在,都會建立新事務允許方法
     * 這樣新事務就可以擁有新的鎖和隔離級別等特性,與當前事務相互獨立
     */
    REQUIRES_NEW(3),
    /**
     * 不支援事務,當前存在事務時,將掛起事務,執行方法
     */
    NOT_SUPPORTED(4),
    /**
     * 不支援事務,如果當前方法存在事務,將丟擲異常,否則繼續使用無事務機制執行
     */
    NEVER(5),
    /**
     * 在當前方法呼叫子方法時,如果子方法發生異常
     * 只回滾子方法執行過的SQL,而不回滾當前方法的事務
     */
    NESTED(6);
    ......
}

日常開發中基本只會使用到REQUIRED(0),REQUIRES_NEW(3),NESTED(6)三種。

NESTEDREQUIRES_NEW是有區別的。NESTED傳播行為會沿用當前事務的隔離級別和鎖等特性,而REQUIRES_NEW則可以擁有自己獨立的隔離級別和鎖等特性。

NESTED的實現主要依賴於資料庫的儲存點(SAVEPOINT)技術,SAVEPOINT記錄了一個儲存點,可以通過ROLLBACK TO SAVEPOINT來回滾到某個儲存點。如果資料庫支援儲存點技術時就啟用儲存點技術;如果不支援就會新建一個事務去執行程式碼,也就相當於REQUIRES_NEW

Transactional自呼叫失效

如果一個類中自身方法的呼叫,我們稱之為自呼叫。如一個訂單業務實現類OrderServiceImpl中有methodA方法呼叫了自身類的methodB方法就是自呼叫,如:

@Transactional
public void methodA(){
    for (int i = 0; i < 10; i++) {
        methodB();
    }
}
    
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
public int methodB(){
    ......
}

在上面方法中不管methodB如何設定隔離級別和傳播行為都是不生效的。即自呼叫失效。

這主要是由於@Transactional的底層實現原理是基於AOP實現,而AOP的原理是動態代理,在自呼叫的過程中是類自身的呼叫,而不是代理物件去呼叫,那麼就不會產生AOP,於是就發生了自呼叫失敗的現象。

要克服這個問題,有2種方法:

  • 編寫兩個Service,用一個Service的methodA去呼叫另外一個Service的methodB方法,這樣就是代理物件的呼叫,不會有問題;
  • 在同一個Service中,methodA不直接呼叫methodB,而是先從Spring IOC容器中重新獲取代理物件`OrderServiceImpl·,獲取到後再去呼叫methodB。說起來有點亂,還是show you the code。
public class OrderServiceImpl implements OrderService,ApplicationContextAware {
    private ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Transactional
    public void methodA(){
        OrderService orderService = applicationContext.getBean(OrderService.class);
        for (int i = 0; i < 10; i++) {
            orderService.methodB();
        }
    }

    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
    public int methodB(){
        ......
    }

}

上面程式碼中我們先實現了ApplicationContextAware介面,然後通過applicationContext.getBean()獲取了OrderService的介面物件。這個時候獲取到的是一個代理物件,也就能正常使用AOP的動態代理了。

回到最開始的那個問題,看完這篇文章是不是有答案了呢