Springboot通過@Transactional管理事務

1. 數據庫事務

事務(Transaction),指訪問並可能更新數據庫中各種數據項的一個程序執行單元(unit),它通常由高級數據庫操縱語言或編程語言(如SQL,C++或Java)書寫的用戶程序的執行所引起。當在數據庫中更改數據成功時,在事務中更改的數據便會提交,不再改變。否則,事務就取消或者回滾,更改無效。

例如 網上購物,其交易過程至少包括以下幾個步驟的操作:

  • 更改客戶所購商品的庫存信息;
  • 保存客戶付款信息;
  • 生成訂單並且保存到數據庫中;
  • 更改用戶相關信息,例如購物數量等。

在正常情況下,這些操作都將順利進行,最終交易成功,與交易相關的所有數據庫信息也成功地更新。但是,如果在執行的途中遇到突然斷電或者其他意外情況,導致這一系列過程中任何一個環節出了差錯,例如在更細商品庫存信息時發生異常、顧客銀行賬戶餘額不足等,都將導致整個交易過程失敗。而一旦失敗,我們需要保持數據庫的狀態不被失敗的交易影響:即原有的庫存信息沒有被更新、用戶也沒有付款、訂單也沒有生成。否則,數據庫的信息將會不一致(如莫名其妙少了1的庫存),或者出現更爲嚴重的不可預測的後果。數據庫事務正是用來保證這種情況下交易的平穩性和可預測性的技術。

1.1 事務特性

ACID 表示事務的特性:原子性、一致性、隔離性和持久性。

  • 原子性(Atomic):事務中各項操作,要麼全做要麼全不做,任何一項操作的失敗都會
    導致整個事務的失敗;
  • 一致性(Consistent):事務結束後系統狀態是一致的;
  • 隔離性(Isolated):併發執行的事務彼此無法看到對方的中間狀態;
  • 持久性(Durable):事務完成後所做的改動都會被持久化,即使發生災難性的失敗。通
    過日誌和同步備份可以在故障發生後重建數據。

注意:嚴格而言,數據庫事務屬性都是由數據庫管理系統來進行保證的,在整個應用程序的運行過程中,應用程序無須去考慮數據庫的ACID實現。

一般情況下,通過執行COMMIT(提交)或ROLLBACK(回滾)語句來終止事務。當執行COMMIT語句時,自從事務啓動以來對數據庫所做的一切更改就成爲永久性的,即被寫入到磁盤,而當執行ROLLBACK語句時,自從事務啓動以來對數據庫所做的一切更改都會被撤銷,並且數據庫中內容返回到事務開始之前所處的狀態。無論什麼情況,在事務完成時,都能保證回到一致性狀態。

1.2 併發控制

數據庫系統,一個明顯的特點是多個用戶共享數據庫資源,尤其是多個用戶可以同時存取相同數據。保證事務ACID的特性是事務處理的重要任務,而併發操作有可能會破壞其ACID特性。

而DBMS併發控制機制的責任是:對併發操作進行正確調度,保證事務的隔離更一般,確保數據庫的一致性。

如果沒有鎖定且多個用戶同時訪問一個數據庫,則當他們的事務同時使用相同的數據時可能會發生問題。由於併發操作帶來的數據不一致性包括:

  • 髒讀(Dirty Read): A 事務讀取 B 事務尚未提交的數據並在此基礎上操作,而 B 事務執行回滾,那麼 A 讀取到的數據就是髒數據。
  • 幻讀(Phantom Read):事務 A 重新執行一個查詢,返回一系列符合查詢條件的行,發現其中插入了被事務 B 提交的行(幻讀的重點在於新增或者刪除 (數據條數變化)同樣的條件, 第1次和第2次讀出來的記錄數不一樣)。
  • 不可重複讀(Unrepeatable Read):事務 A 重新讀取前面讀取過的數據,發現該數據已經被另一個已提交的事務 B 修改過了(不可重複讀的重點是修改,同樣的條件, 你讀取過的數據, 再次讀取出來發現值不一樣了)。
1.3 事務隔離級別

爲了避免上面出現的幾種情況,在標準SQL規範中,定義了4個事務隔離級別,不同的隔離級別對事務的處理不同。

  • 讀未提交(Read Uncommitted):如果一個事務已經開始寫數據,則不允許其他事務同時進行寫操作,但允許其他事務讀此行數據。最低的隔離級別,最直接的效果就是一個事務可以讀取另一個事務並未提交的更新結果。

  • 讀提交(Read Committed):通常是大部分數據庫採用的默認隔離級別,它在Read Uncommitted隔離級別基礎上所做的限定更進一步, 在該隔離級別下,一個事務的更新操作結果只有在該事務提交之後,另一個事務纔可能讀取到同一筆數據更新後的結果。 所以,Read Committed可以避免Read Uncommitted隔離級別下存在的髒讀問題, 但無法避免不可重複讀取和幻讀的問題。

  • 可重複讀取(Repeatable Read):Repeatable Read隔離級別可以保證在整個事務的過程中,對同一筆數據的讀取結果是相同的,不管其他事務是否同時在對同一筆數據進行更新,也不管其他事務對同一筆數據的更新提交與否。 Repeatable Read隔離級別避免了髒讀和不可重複讀取的問題,但無法避免幻讀。(mysql默認隔離級別)

  • 序列化(Serializable):最爲嚴格的隔離級別,所有的事務操作都必須依次順序執行,可以避免其他隔離級別遇到的所有問題,是最爲安全的隔離級別, 但同時也是性能最差的隔離級別,因爲所有的事務在該隔離級別下都需要依次順序執行,所以,併發度下降,吞吐量上不去,性能自然就下來了。 因爲該隔離級別極大的影響系統性能,所以,很少場景會使用它。通常情況下,我們會使用其他隔離級別加上相應的併發鎖的機制來控制對數據的訪問,這樣既保證了系統性能不會損失太大,也能夠一定程度上保證數據的一致性。

2. @Transactional

Spring 爲事務管理提供了豐富的功能支持。Spring 事務管理分爲編碼式和聲明式的兩種方式。編程式事務指的是通過編碼方式實現事務;聲明式事務基於 AOP,將具體業務邏輯與事務處理解耦。聲明式事務管理使業務代碼邏輯不受污染, 因此在實際使用中聲明式事務用的比較多。聲明式事務有兩種方式,一種是在配置文件中做相關的事務規則聲明,另一種是基於@Transactional 註解的方式。
使用@Transactional的相比傳統的我們需要手動開啓事務,然後提交事務來說。它提供如下方便:

  • 根據你的配置,設置是否自動開啓事務
  • 自動提交事務或者遇到異常自動回滾

由於在Spring boot中使用到的是mybatis,會自動配置一個DataSourceTransactionManager,所以我們只需在方法(或者類)加上 @Transactional註解,就自動納入 Spring 的事務管理了。

2.1 @Transactional使用

@Transactional 可以作用於接口、接口方法、類以及類方法上。當作用於類上時,該類的所有 public 方法將都具有該類型的事務屬性,同時,我們也可以在方法級別使用該標註來覆蓋類級別的定義。

雖然 @Transactional 註解可以作用於接口、接口方法、類以及類方法上,但是 Spring 建議不要在接口或者接口方法上使用該註解,因爲這隻有在使用基於接口的代理時它纔會生效。另外, @Transactional 註解應該只被應用到 public 方法上,這是由 Spring AOP 的本質決定的。如果你在 protected、private 或者默認可見性的方法上使用@Transactional 註解,這將被忽略,也不會拋出任何異常。

默認情況下,只有來自外部的方法調用纔會被AOP代理捕獲,也就是,類內部方法調用本類內部的其他方法並不會引起事務行爲,即使被調用方法使用@Transactional註解進行修飾:

/*
* 情況一:都有事務註解,異常在子方法出現,事務生效
*/
@Override
@Transactional
public Long addBook(Book book) {
    Long result = add(book);
    return result;
}

@Transactional
public Long add(Book book){
    Long result =  bookDao.addBook(book);
    int i = 1/0;
    return result;
}

/*
* 情況二:都有事務註解,異常在主方法出現,事務生效
*/
@Override
@Transactional
public Long addBook(Book book) {
    Long result = add(book);
    int i = 1/0;
    return result;
}

@Transactional
public Long add(Book book){
    Long result =  bookDao.addBook(book);
    return result;
}

/*
* 情況三:只有主方法有事務註解,異常在子方法出現,事務生效
*/
@Override
@Transactional
public Long addBook(Book book) {
    Long result = add(book);
    return result;
}

public Long add(Book book){
    Long result =  bookDao.addBook(book);
    int i = 1/0;
    return result;
}

/*
* 情況四:只有主方法有事務註解,異常在主方法出現,事務生效
*/
@Override
@Transactional
public Long addBook(Book book) {
    Long result = add(book);
    int i = 1/0;
    return result;
}

public Long add(Book book){
    Long result =  bookDao.addBook(book);
    return result;
}

/*
* 情況五:只有子方法有事務註解,異常在子方法出現,事務不生效
*/
@Override
public Long addBook(Book book) {
    Long result = add(book);
    return result;
}

@Transactional
public Long add(Book book){
    Long result =  bookDao.addBook(book);
    int i = 1/0;
    return result;
}
2.2 注意事項

Spring的事務管理默認是針對unchecked exception回滾,也就是默認對Error異常RuntimeException異常以及其子類進行事務回滾,且必須拋出異常(拋出異常之後,事務會自動回滾,數據不會插入到數據庫。),若使用try-catch對其異常捕獲則不會進行回滾!(Error異常和RuntimeException異常拋出時不需要方法調用throws或try-catch語句));而checked exception 則必須用try語句塊進行處理或者把異常交給上級方法處理總之就是必須寫代碼處理它。

但是我們平時做業務處理時,需要捕獲異常,所以可以手動拋出RuntimeException異常或者添加rollbackFor = Exception.class(也可以指定相應異常)來解決這個問題。

/*
 * 捕獲異常時,要想使事務生效,需要手動拋出RuntimeException異常或者添加rollbackFor = Exception.class
*/
@Override
@Transactional
public Long addBook(Book book) {
    Long result = null;
    try {
        result = bookDao.addBook(book);
        int i = 1/0;
    } catch (Exception e) {
        e.printStackTrace();
        throw new RuntimeException();
    }
    return result;
}

@Override
@Transactional(rollbackFor = Exception.class)
public Long addBook(Book book) {
    Long result = null;
    try {
        result = bookDao.addBook(book);
        int i = 1/0;
    } catch (Exception e) {
        e.printStackTrace();
        throw e;
    }
    return result;
}

示例代碼參考:https://blog.csdn.net/u013929527/article/details/102596243

2.3 @Transactional註解屬性介紹及使用

value 和 transactionManager 屬性:

它們兩個是一樣的意思。當配置了多個事務管理器時,可以使用該屬性指定選擇哪個事務管理器。

propagation 屬性:

事務的傳播行爲,默認值爲 Propagation.REQUIRED。可選的值有:

  • Propagation.REQUIRED:
    如果當前存在事務,則加入該事務,如果當前不存在事務,則創建一個新的事務。如a方法和b方法都添加了註解,使用默認傳播模式,則a方法內部調用b方法,會把兩個方法的事務合併爲一個事務。
    這裏又會存在問題,如果b方法內部拋了異常,而a方法catch了b方法的異常,那這個事務還能正常運行嗎?答案是不行!會拋出異常org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only,因爲當ServiceB中拋出了一個異常以後,ServiceB會把當前的transaction標記爲需要rollback。但是ServiceA中捕獲了這個異常,並進行了處理,認爲當前transaction應該正常commit。此時就出現了前後不一致,也就是因爲這樣,拋出了前面的UnexpectedRollbackException。
  • Propagation.SUPPORTS:
    如果當前存在事務,則加入該事務;如果當前不存在事務,則以非事務的方式繼續運行。
    Propagation.MANDATORY:
    如果當前存在事務,則加入該事務;如果當前不存在事務,則拋出異常。
  • Propagation.REQUIRES_NEW:
    重新創建一個新的事務,如果當前存在事務,暫停當前的事務。這個屬性可以實現:類A中的a方法加上默認註解@Transactional(propagation = Propagation.REQUIRED),類B中的b方法加上註解@Transactional(propagation = Propagation.REQUIRES_NEW),然後在a方法中調用b方法操作數據庫,再在a方法最後拋出異常,會發現a方法中的b方法對數據庫的操作沒有回滾,因爲Propagation.REQUIRES_NEW會暫停a方法的事務。
  • Propagation.NOT_SUPPORTED:
    以非事務的方式運行,如果當前存在事務,暫停當前的事務。
  • Propagation.NEVER:
    以非事務的方式運行,如果當前存在事務,則拋出異常。
  • Propagation.NESTED:
    和 Propagation.REQUIRED 效果一樣。

isolation 屬性:

事務的隔離級別,默認值爲 Isolation.DEFAULT。可選的值有:

  • Isolation.DEFAULT:使用底層數據庫默認的隔離級別。
  • Isolation.READ_UNCOMMITTED
  • Isolation.READ_COMMITTED
  • Isolation.REPEATABLE_READ
  • Isolation.SERIALIZABLE

timeout 屬性:

事務的超時時間,默認值爲-1。如果超過該時間限制但事務還沒有完成,則自動回滾事務。

readOnly 屬性:

指定事務是否爲只讀事務,默認值爲 false;爲了忽略那些不需要事務的方法,比如讀取數據,可以設置 read-only 爲 true。

rollbackFor 屬性:

用於指定能夠觸發事務回滾的異常類型,可以指定多個異常類型。

noRollbackFor 屬性:

拋出指定的異常類型,不回滾事務,也可以指定多個異常類型。

參考文章:https://blog.csdn.net/Abysscarry/article/details/80189232

2.4 @Transactional使用建議
  • 一個功能是否要事務,必須納入設計、編碼考慮。不能僅僅完成了基本功能就ok。
  • 如果加了事務,必須做好開發環境測試(測試環境也儘量觸發異常、測試回滾),確保事務生效。
  • 不要在接口上聲明@Transactional ,而要在具體類的方法上使用 @Transactional 註解,否則註解可能無效。
  • 不要圖省事,將@Transactional放置在類級的聲明中,放在類聲明,會使得所有方法都有事務。故@Transactional應該放在方法級別,不需要使用事務的方法,就不要放置事務,比如查詢方法。否則對性能是有影響的。
  • 最後有個關鍵的一點:和鎖同時使用需要注意:由於Spring事務是通過AOP實現的,所以在方法執行之前會有開啓事務,之後會有提交事務邏輯。而synchronized代碼塊執行是在事務之內執行的,可以推斷在synchronized代碼塊執行完時,事務還未提交,其他線程進入synchronized代碼塊後,讀取的數據不是最新的。
    所以必須使synchronized鎖的範圍大於事務控制的範圍,把synchronized加到Controller層或者大於事務邊界的調用層!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章