當前位置: 妍妍網 > 碼農

地表最強SpringBoot 各種回滾實戰

2024-03-23碼農

概念

事務定義

事務,就是一組操作資料庫的動作集合。事務是現代資料庫理論中的核心概念之一。如果一組處理步驟或者全部發生或者一步也不執行,我們稱該組處理步驟為一個事務。當所有的步驟像一個操作一樣被完整地執行,我們稱該事務被送出。由於其中的一部份或多步執行失敗,導致沒有步驟被送出,則事務必須回滾到最初的系統狀態。

事務特點

  • 原子性: 一個事務中所有對資料庫的操作是一個不可分割的操作序列,要麽全做要麽全不做

  • 一致性: 數據不會因為事務的執行而遭到破壞

  • 隔離性: 一個事務的執行,不受其他事務的幹擾,即並行執行的事務之間互不幹擾

  • 永續性: 一個事務一旦送出,它對資料庫的改變就是永久的。

  • 事務實作機制

    Spring 為事務管理提供了豐富的功能支持。Spring 事務管理分為編碼式和聲明式的兩種方式。

  • 編程式事務管理: 編程式事務管理使用 TransactionTemplate 或者直接使用底層的 PlatformTransactionManager 。對於編程式事務管理,spring推薦使用 TransactionTemplate

  • 聲明式事務管理: 建立在AOP之上的。其本質是對方法前後進行攔截,然後在目標方法開始之前建立或者加入一個事務,在執行完目標方法之後根據執行情況送出或者回滾事務。

  • 聲明式事務管理不需要入侵程式碼,更快捷而且簡單,推薦使用。

    聲明式事務有兩種方式:

  • 一種是在配置檔(xml)中做相關的事務規則聲明(因為很少用本文不講解)

  • 另一種是基於 @Transactional 註解的方式。註釋配置是目前流行的使用方式,推薦使用。

  • 在套用系統呼叫聲明了 @Transactional 的目標方法時,Spring Framework 預設使用 AOP 代理,在程式碼執行時生成一個代理物件,根據 @Transactional 的內容配置資訊,這個代理物件決定該聲明 @Transactional 的目標方法是否由攔截器 TransactionInterceptor 來使用攔截,在 TransactionInterceptor 攔截時,會在目標方法開始執行之前建立並加入事務,並執行目標方法的邏輯,最後根據執行情況是否出現異常,利用抽象事務管理器 AbstractPlatformTransactionManager 操作資料來源 DataSource 送出或回滾事務。

    Spring AOP 代理有 CglibAopProxy JdkDynamicAopProxy 兩種,以 CglibAopProxy 為例,對於 CglibAopProxy ,需要呼叫其內部類的 DynamicAdvisedInterceptor intercept 方法。對於 JdkDynamicAopProxy ,需要呼叫其 invoke 方法。

    開啟事務

    註解@Transactional的使用

    註解@Transactional常用配置

    圖片

    Propagation的內容(事務的傳播行為)

    例如: @Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)

    圖片

    事務5種隔離級別

    例如: @Transactional(isolation = Isolation.READ_COMMITTED)

    圖片

    使用註意事項(防止事務失效)

    1.在具體的類(或類的方法)上使用 @Transactional 註解,而不要使用在類所要實作的任何介面上。

    2. @Transactional 註解應該只被套用在 public 修飾的方法上。如果你在 protected private 或者 package-visible 的方法上使用 該註解,它也不會報錯(IDEA會有提示), 但事務並沒有生效。

    3.被外部呼叫的公共方法A有兩個進行了數據操作的子方法B和子方法C的事務註解說明:

  • 被外部呼叫的公共方法A未聲明事務 @Transactional ,子方法B和C若是其他類的方法且各自聲明事務,則事務由子方法B和C各自控制

  • 被外部呼叫的公共方法A未聲明事務 @Transactional ,子方法B和C若是本類的方法,則無論子方法B和C是否聲明事務,事務均不會生效

  • 被外部呼叫的公共方法A聲明事務 @Transactional ,無論子方法B和C是不是本類的方法,無論子方法B和C是否聲明事務,事務均由公共方法A控制

  • 被外部呼叫的公共方法A聲明事務 @Transactional ,子方法執行異常,但執行異常被子方法自己 try-catch 處理了,則事務回滾是不會生效的!

  • 如果想要事務回滾生效,需要將子方法的事務控制交給呼叫的方法來處理:

  • 方案1:子方法中不用 try-catch 處理執行異常

  • 方案2:子方法的catch裏面將執行異常丟擲【 throw new RuntimeException();

  • 4.預設情況下,Spring會對unchecked異常進行事務回滾,也就是預設對 RuntimeException() 異常或是其子類別進行事務回滾。如果是checked異常則不回滾,例如空指標異常、算數異常等會被回滾;檔讀寫、網路問題Spring就沒法回滾。

    若想對所有異常(包括自訂異常)都起作用,註解上面需配置異常型別: @Transactional(rollbackFor = Exception. class

    5.資料庫要支持事務,如果是mysql,要使用innodb引擎,myisam不支持事務

    6.事務 @Transactional 由spring控制時,它會在丟擲異常的時候進行回滾。如果自己使用try-catch捕獲處理了,是不生效的。如果想事務生效可以進行手動回滾或者在catch裏面將異常丟擲【 throw new RuntimeException();

    方案一:手動丟擲執行時異常(缺陷是不能在catch程式碼塊自訂返回值)

    try{
    ....
    }catch(Exception e){
    logger.error("",e);
    throw new RuntimeException(e);
    }

    方案二:手動進行回滾【 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

    try{
    ...
    }catch(Exception e){
    log.error("fail",e);
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    returnfalse;
    }

    @Transactional 可以放在Controller下面直接起作用,看到網上好多同學說要放到 @Component 下面或者 @Service 下面,經過試驗,可以不用放在這兩個下面也起作用。

    @Transactional 引入包問題,它有兩個包:

    import javax.transaction.Transactional; 
    // 和
    import org.springframework.transaction.annotation.Transactional; // 推薦

    這兩個都可以用,對比了一下他們兩個的方法和內容,發現後面的比前面的強大。建議使用後面的。

    使用場景

    自動回滾

    直接丟擲,不try/catch

    @Override
    @Transactional(rollbackFor = Exception. class)
    public Object submitOrder() throws Exception {
    success();
    //假如exception這個操作資料庫的方法會丟擲異常,方法success()對資料庫的操作會回滾。 
    exception(); 
    return ApiReturnUtil.success();
    }

    手動回滾

    進行try/catch,回滾並丟擲

    @Override
    @Transactional(rollbackFor = Exception. class)
    public Object submitOrder (){
    success();
    try {
    exception(); 
    } catch (Exception e) {
    e.printStackTrace();
    // 手動回滾事務
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    return ApiReturnUtil.error();
    }
    return ApiReturnUtil.success();
    }

    回滾部份異常

    使用【 Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint(); 】設定回滾點。

    使用【 TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint); 】回滾到savePoint。

    @Override
    @Transactional(rollbackFor = Exception. class)
    public Object submitOrder (){
    success();
    //只回滾以下異常,
    Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
    try {
    exception(); 
    } catch (Exception e) {
    e.printStackTrace();
    // 手工回滾事務
    TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
    return ApiReturnUtil.error();
    }
    return ApiReturnUtil.success();
    }

    手動建立、送出、回滾事務

    PlatformTransactionManager 這個介面中定義了三個方法 getTransaction 建立事務,commit 送出事務,rollback 回滾事務。它的實作類是 AbstractPlatformTransactionManager

    @Autowired
    priDataSourceTransactionManager dataSourceTransactionManager;
    @Autowired
    TransactionDefinition transactionDefinition;
    // 手動建立事務
    TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
    // 手動送出事務
    dataSourceTransactionManager.commit(transactionStatus);
    // 手動回滾事務。(最好是放在catch 裏面,防止程式異常而事務一直卡在哪裏未送出)
    dataSourceTransactionManager.rollback(transactionStatus);

    事務失效不回滾的原因及解決方案

    異常被捕獲導致事務失效

    在spring boot 中,使用事務非常簡單,直接在方法上面加入 @Transactional 就可以實作。

    @GetMapping("delete")
    @ResponseBody
    @Transactional
    public void delete(@RequestParam("id") int id) {
    try {
    //delete country
    this.repository.delete(id);
    if(id == 1){
    throw Exception("測試事務");
    }
    //delete city
    this.repository.deleteByCountryId(id);
    }catch (Exception e){
    logger.error("delete false:" + e.getMessage());
    }
    }

    發現事務不回滾,即 this.repository.delete(id); 成功把數據刪除了。

    原因:

    預設spring事務只在發生未被捕獲的 RuntimeException 時才回滾。

    spring aop 異常捕獲原理:被攔截的方法需顯式丟擲異常,並不能經任何處理,這樣aop代理才能捕獲到方法的異常,才能進行回滾,預設情況下aop只捕獲 RuntimeException 的異常,但可以透過配置來捕獲特定的異常並回滾。

    換句話說在service的方法中不使用 try catch 或者在 catch 中最後加上 throw new RuntimeExcetpion() 丟擲執行異常,這樣程式異常時才能被aop捕獲進而回滾。

    解決方案:

    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>4.3.2.RELEASE</version>
    </dependency>
    <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.9</version>
    </dependency>
    <plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.9</version>
    <configuration>
    <showWeaveInfo>true</showWeaveInfo>
    <aspectLibraries>
    <aspectLibrary>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    </aspectLibrary>
    </aspectLibraries>
    </configuration>
    <executions>
    <execution>
    <goals>
    <goal>compile</goal>
    <goal>test-compile</goal>
    </goals>
    </execution>
    </executions>
    </plugin>

    解決方案

    方案1、在類上(或者最外層的公共方法)加事務

    @Service
    @Slf4j
    public class MyTransactional {
    // 最外層公共方法。自動回滾事務方式,insertOrder()方法報錯後事務回滾,且執行緒中止,後續邏輯無法執行
    @Transactional
    public void test1() {
    this.insertOrder();
    System.out.println("11111111111111111");
    }
    // 最外層公共方法。手動回滾事務方式,insertOrder()方法報錯後事務回滾,可以繼續執行後續邏輯
    @Transactional
    public void test2() {
    try {
    insertOrder();
    } catch (Exception e) {
    log.error("faild to ...", e);
    // 手動回滾事務
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    // 其他操作
    }
    // 其他操作
    }
    // 進行資料庫操作的方法(private 或 public 均可)
    private void insertOrder() {
    //insert log info
    //insertOrder
    //updateAccount
    }
    }

    方案 2、使用AspectJ 取代 Spring AOP 代理

    上面的兩個問題 @Transactional 註解只套用到 public 方法和自呼叫問題,是由於使用 Spring AOP 代理造成的。為解決這兩個問題,可以使用 AspectJ 取代 Spring AOP 代理。

    需要將下面的 AspectJ 資訊添加到 xml 配置資訊中。

    AspectJ 的 xml 配置資訊

    <tx:annotation-driven mode="aspectj" />
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
    </bean>
    </bean class="org.springframework.transaction.aspectj.AnnotationTransactionAspect" factory-method="aspectOf">
    <property name="transactionManager" ref="transactionManager" />
    </bean> 

    同時在 Maven 的 pom 檔中加入 spring-aspects aspectjrt dependency 以及 aspectj-maven-plugin

    AspectJ 的 pom 配置資訊

  • 方案1:例如service層處理事務,那麽service中的方法中不做異常捕獲,或者在catch語句中最後增加 throw new RuntimeException(); 語句,以便讓aop捕獲異常再去回滾,並且在service的上層要繼續捕獲這個異常。

  • 方案2:在service層方法的catch語句中進行手動回滾,這樣上層就無需去處理異常。

  • @GetMapping("delete"
    @ResponseBody 
    @Transactional 
    public Object delete(@RequestParam("id") int id){ 
    if (id < 1){
    return new MessageBean(101,"parameter wrong: id = " + id) ; 

    try { 
    //delete country
    this.countryRepository.delete(id);
    //delete city
    this.cityRepository.deleteByCountryId(id);
    return new MessageBean(200,"delete success");
    }catch (Exception e){
    logger.error("delete false:" + e.getMessage());
    // 手動回滾
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    return new MessageBean(101,"delete false");
    }
    }

    自呼叫導致事務失效

    問題描述及原因

    在 Spring 的 AOP 代理下,只有目標方法由外部呼叫,目標方法才由 Spring 生成的代理物件來管理,否則會造成自呼叫問題。

    若同一類中的 沒有 @Transactional 註解的方法 內部呼叫 有 @Transactional 註解的方法,有 @Transactional 註解的方法的事務被忽略,不會發生回滾。見 範例程式碼展示。

    自呼叫問題範例:

    @Service
    public class OrderService {
    private void insert() {
    insertOrder();
    }
    @Transactional
    public void insertOrder() {
    //insert log info
    //insertOrder
    //updateAccount
    }
    }
    // insertOrder() 盡管有@Transactional 註解,但它被內部方法 insert()呼叫,事務被忽略,出現異常事務不會發生回滾,並且會報錯類似於:org.springframework.transaction.NoTransactionException: No transaction aspect-managed TransactionStatus in scope(轉譯:沒有Transaction無法回滾事務。自呼叫導致@Transactional 失效。)

    自呼叫失效原因:

    spring裏事務是用註解配置的,當一個方法沒有介面,單單只是一個內部方法時,事務的註解是不起作用的,需要回滾時就會報錯。

    出現這個問題的根本原因是:

    @Transactional 的實作原理是AOP,AOP的實作原理是動態代理,而自呼叫時並不存在代理物件的呼叫,也就不會產生基於AOP 的事務回滾操作

    雖然可以直接從容器中獲取代理物件,但這樣有侵入之嫌,不推薦。

    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>4.3.2.RELEASE</version>
    </dependency>
    <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.9</version>
    </dependency>
    <plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.9</version>
    <configuration>
    <showWeaveInfo>true</showWeaveInfo>
    <aspectLibraries>
    <aspectLibrary>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    </aspectLibrary>
    </aspectLibraries>
    </configuration>
    <executions>
    <execution>
    <goals>
    <goal>compile</goal>
    <goal>test-compile</goal>
    </goals>
    </execution>
    </executions>
    </plugin>

    其他

    事務送出方式

    預設情況下,資料庫處於自動送出模式。每一條語句處於一個單獨的事務中,在這條語句執行完畢時,如果執行成功則隱式的送出事務,如果執行失敗則隱式的回滾事務。

    對於正常的事務管理,是一組相關的操作處於一個事務之中,因此必須關閉資料庫的自動送出模式。不過,這個我們不用擔心,spring 會將底層連線的【自動送出特性】設定為 false 。

    也就是在使用 spring 進行事務管理的時候,spring 會將【是否自動送出】設定為false,等價於JDBC中的 connection.setAutoCommit(false); ,在執行完之後在進行送出 connection.commit();

    事務回滾規則

    指示spring事務管理器回滾一個事務的推薦方法是在當前事務的上下文內丟擲異常。spring事務管理器會捕捉任何未處理的異常,然後依據規則決定是否回滾丟擲異常的事務。

    預設配置下,spring只有在丟擲的異常為執行時unchecked異常時才回滾該事務,也就是丟擲的異常為 RuntimeException 的子類別(Errors也會導致事務回滾),而丟擲checked異常則不會導致事務回滾。

    可以明確的配置在丟擲那些異常時回滾事務,包括checked異常。也可以明確定義那些異常丟擲時不回滾事務。

    事務並行會產生的問題

    圖片

    第一類遺失更新

    在沒有事務隔離的情況下,兩個事務都同時更新一行數據,但是第二個事務卻中途失敗結束, 導致對數據的兩個修改都失效了。

    例如:

    張三的薪資為5000,事務A中獲取薪資為5000,事務B獲取薪資為5000,匯入100,並送出資料庫,薪資變為5100;

    隨後,事務A發生異常,回滾了,恢復張三的薪資為5000,這樣就導致事務B的更新遺失了。

    臟讀

    臟讀就是指當一個事務正在存取數據,並且對數據進行了修改,而這種修改還沒有送出到資料庫中,這時,另外一個事務也存取這個數據,然後使用了這個數據。

    例如:

    張三的薪資為5000,事務A中把他的薪資改為8000,但事務A尚未送出。

    與此同時,事務B正在讀取張三的薪資,讀取到張三的薪資為8000。

    隨後,事務A發生異常,回滾了事務,張三的薪資又回滾為5000。

    最後,事務B讀取到的張三薪資為8000的數據即為臟數據,事務B做了一次臟讀。

    不可重復讀

    是指在一個事務內,多次讀同一數據。在這個事務還沒有結束時,另外一個事務也存取該同一數據。那麽,在第一個事務中的兩次讀數據之間,由於第二個事務的修改,那麽第一個事務兩次讀到的的數據可能是不一樣的。這樣就發生了在一個事務內兩次讀到的數據是不一樣的,因此稱為是不可重復讀。

    例如:

    在事務A中,讀取到張三的薪資為5000,操作沒有完成,事務還沒送出。

    與此同時,事務B把張三的薪資改為8000,並送出了事務。

    隨後,在事務A中,再次讀取張三的薪資,此時薪資變為8000。在一個事務中前後兩次讀取的結果並不致,導致了不可重復讀。

    第二類遺失更新

    不可重復讀的特例。

    有兩個並行事務同時讀取同一行數據,然後其中一個對它進行修改送出,而另一個也進行了修改送出。這就會造成第一次寫操作失效。

    例如:

    在事務A中,讀取到張三的存款為5000,操作沒有完成,事務還沒送出。

    與此同時,事務B存入1000,把張三的存款改為6000,並送出了事務。

    隨後,在事務A中,儲存500,把張三的存款改為5500,並送出了事務,這樣事務A的更新覆蓋了事務B的更新。

    幻讀

    是指當事務不是獨立執行時發生的一種現象,例如第一個事務對一個表中的數據進行了修改,這種修改涉及到表中的全部數據行。同時,第二個事務也修改這個表中的數據,這種修改是向表中插入一行新數據。那麽,以後就會發生操作第一個事務的使用者發現表中還有沒有修改的數據行,就好象發生了幻覺一樣。

    例如:

    目前薪資為5000的員工有10人,事務A讀取到所有的薪資為5000的人數為10人。

    此時,事務B插入一條薪資也為5000的記錄。

    這時,事務A再次讀取薪資為5000的員工,記錄為11人。此時產生了幻讀。

    不可重復讀和幻讀的區別

    不可重復讀的重點是修改,同樣的條件,你讀取過的數據,再次讀取出來發現值不一樣了

    幻讀的重點在於新增或者刪除,同樣的條件,第 1 次和第 2 次讀出來的記錄數不一樣。

    <END>