Spring @Transactional 事務機制


幾個概念要清楚:事務的傳播機制,事務的邊界

 

工作原理

運行配置@Transactional注解的測試類的時候,具體會發生如下步驟

1)事務開始時,通過AOP機制,生成一個代理connection對象,並將其放入DataSource實例的某個與DataSourceTransactionManager相關的某處容器中。在接下來的整個事務中,客戶代碼都應該使用該connection連接數據庫,執行所有數據庫命令[不使用該connection連接數據庫執行的數據庫命令,在本事務回滾的時候得不到回滾]

2)事務結束時,回滾在第1步驟中得到的代理connection對象上執行的數據庫命令,然后關閉該代理connection對象

 

 

@Transactional不生效的情況

  1. @Transactional  在private 方法是不生效
  2. 在同一個bean里,嵌套的public方法@Transactional 也不生效

解決方法

      簡單粗暴,有需要就在調用service中就開@Transactional,如果實在不行,像里面有調用第三方資源,唔想事務開得太久,可以考慮新建另外一個service去開@Transactional。

 

 

 

 

那么@Transactional如何工作?

 

實現了EntityManager接口的持久化上下文代理並不是聲明式事務管理的唯一部分,事實上包含三個組成部分:

1. EntityManager Proxy本身

2. 事務的切面

3. 事務管理器

 

事務的切面

事務的切面是一個“around(環繞)”切面,在注解的業務方法前后都可以被調用。實現切面的具體類是TransactionInterceptor。

 

事務的切面有兩個主要職責:

1. 在’before’時,切面提供一個調用點,來決定被調用業務方法應該在正在進行事務的范圍內運行,還是開始一個新的獨立事務。

2. 在’after’時,切面需要確定事務被提交,回滾或者繼續運行。

3. 在’before’時,事務切面自身不包含任何決策邏輯,是否開始新事務的決策委派給事務管理器完成。

 

 

事務管理器

事務管理器需要解決下面兩個問題:

新的Entity Manager是否應該被創建?

是否應該開始新的事務?

 

這些需要事務切面’before’邏輯被調用時決定。事務管理器的決策基於以下兩點:

1. 事務是否正在進行

2. 事務方法的propagation屬性(比如REQUIRES_NEW總要開始新事務)

 

如果事務管理器確定要創建新事務,那么將:

1. 創建一個新的entity manager

2. entity manager綁定到當前線程

3. 從數據庫連接池中獲取連接

4. 將連接綁定到當前線程

 

特點:

1. 使用ThreadLocal變量將entity manager和數據庫連接都綁定到當前線程。

2. 事務運行時他們存儲在線程中,當它們不再被使用時,事務管理器決定是否將他們清除。

3. 程序的任何部分如果需要當前的entity manager和數據庫連接都可以從線程中獲取。

 

 

EntityManager proxy

 

EntityManager proxy(前面已經介紹過)就是謎題的最后一部分。當業務方法調用entityManager.persist()時,這不是由entity manager直接調用的。

而是業務方法調用代理,代理從線程獲取當前的entity manager,前面介紹過事務管理器將entity manager綁定到線程。

了解了@Transactional機制的各個部分,我們來看一下實現它的常用Spring配置。

 

 

 

根據上面所述,我們所使用的客戶代碼應該具有如下能力:

1)每次執行數據庫命令的時候

如果在事務的上下文環境中,那么不直接創建新的connection對象,而是嘗試從DataSource實例的某個與DataSourceTransactionManager相關的某處容器中獲取connection對象;在非事務的上下文環境中,直接創建新的connection對象

2)每次執行完數據庫命令的時候

如果在事務的上下文環境中,那么不直接關閉connection對象,因為在整個事務中都需要使用該connection對象,而只是釋放本次數據庫命令對該connection對象的持有;在非事務的上下文環境中,直接關閉該connection對象

 

 

在service類前加上@Transactional,聲明這個service所有方法需要事務管理。每一個業務方法開始時都會打開一個事務。error是一定會回滾的

Spring默認情況下會對運行期例外(RunTimeException)進行事務回滾。這個例外是unchecked

如果遇到checked意外就不回滾。

如何改變默認規則:

1 讓checked例外也回滾:在整個方法前加上 @Transactional(rollbackFor=Exception.class)

2 讓unchecked例外不回滾: @Transactional(notRollbackFor=RunTimeException.class)

3 不需要事務管理的(只查詢的)方法:@Transactional(propagation=Propagation.NOT_SUPPORTED)

4 如果不添加rollbackFor等屬性,Spring碰到Unchecked Exceptions都會回滾,不僅是RuntimeException,也包括Error。

 

注意:

如果異常被try{}catch{}了,事務就不回滾了,如果想讓事務回滾必須再往外拋try{}catch{throw Exception}。

 

 

1.檢查型異常(Checked Exception)

 

  個人理解:所謂檢查(Checked)是指編譯器要檢查這類異常,檢查的目的一方面是因為該類異常的發生難以避免,另一方面就是讓開發者去解決掉這類異常,所以稱為必須處理(try ...catch)的異常。如果不處理這類異常,集成開發環境中的編譯器一般會給出錯誤提示。

 

  例如:一個讀取文件的方法代碼邏輯沒有錯誤,但程序運行時可能會因為文件找不到而拋出FileNotFoundException,如果不處理這些異常,程序將來肯定會出錯。所以編譯器會提示你要去捕獲並處理這種可能發生的異常,不處理就不能通過編譯。

 

2.非檢查型異常(Unchecked Exception)

 

  個人理解:所謂非檢查(Unchecked)是指編譯器不會檢查這類異常,不檢查的則開發者在代碼的編輯編譯階段就不是必須處理,這類異常一般可以避免,因此無需處理(try ...catch)。如果不處理這類異常,集成開發環境中的編譯器也不會給出錯誤提示。

 

  例如:你的程序邏輯本身有問題,比如數組越界、訪問null對象,這種錯誤你自己是可以避免的。編譯器不會強制你檢查這種異常。

 

 

 


 

 

常見坑點

 

使用事務注解@Transactional 之前,應該先了解它的相關屬性,避免在實際項目中踩中各種各樣的坑點。

 

常見坑點1:遇到檢測異常時,事務默認不回滾。

 

例如下面這段代碼,賬戶余額依舊增加成功,並沒有因為后面遇到SQLException(檢測異常)而進行事務回滾!!

  @Transactional
    public void addMoney() throws Exception {
        //先增加余額
        accountMapper.addMoney();
        //然后遇到故障
        throw new SQLException("發生異常了..");
    }

原因分析:因為Spring的默認的事務規則是遇到運行異常(RuntimeException及其子類)和程序錯誤(Error)才會進行事務回滾,顯然SQLException並不屬於這個范圍。如果想針對檢測異常進行事務回滾,可以在@Transactional 注解里使用
rollbackFor 屬性明確指定異常。例如下面這樣,就可以正常回滾:

@Transactional(rollbackFor = Exception.class)
    public void addMoney() throws Exception {
        //先增加余額
        accountMapper.addMoney();
        //然后遇到故障
        throw new SQLException("發生異常了..");
    }

常見坑點2: 在業務層捕捉異常后,發現事務不生效。

這是許多新手都會犯的一個錯誤,在業務層手工捕捉並處理了異常,你都把異常“吃”掉了,Spring自然不知道這里有錯,更不會主動去回滾數據。例如:下面這段代碼直接導致增加余額的事務回滾沒有生效。

 @Transactional
    public void addMoney() throws Exception {
        //先增加余額
        accountMapper.addMoney();
        //謹慎:盡量不要在業務層捕捉異常並處理
        try {
            throw new SQLException("發生異常了..");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

不要小瞧了這些細節,往前暴露異常很大程度上很能夠幫我們快速定位問題,而不是經常在項目上線后出現問題,卻無法刨根知道哪里報錯。

推薦做法:若非實際業務要求,則在業務層統一拋出異常,然后在控制層統一處理。

 @Transactional
    public void addMoney() throws Exception {
        //先增加余額
        accountMapper.addMoney();
        //推薦:在業務層將異常拋出
        throw new RuntimeException("發生異常了..");
    }

 

  • Spring團隊的建議是你在具體的類(或類的方法)上使用 @Transactional 注解,而不要使用在類所要實現的任何接口上。你當然可以在接口上使用 @Transactional 注解,但是這將只能當你設置了基於接口的代理時它才生效。因為注解是不能繼承的,這就意味着如果你正在使用基於類的代理時,那么事務的設置將不能被基於類的代理所識別,而且對象也將不會被事務代理所包裝(將被確認為嚴重的)。因此,請接受Spring團隊的建議並且在具體的類火方法上使用 @Transactional 注解。
  • @Transactional 注解標識的方法,處理過程盡量的簡單。尤其是帶鎖的事務方法,能不放在事務里面的最好不要放在事務里面。可以將常規的數據庫查詢操作放在事務前面進行,而事務內進行增、刪、改、加鎖查詢等操作
  • @Transactional 注解的默認事務管理器bean是“transactionManager”,如果聲明為其他名稱的事務管理器,需要在方法上添加@Transational(“managerName”)。
  • @Transactional 注解標注的方法中不要出現網絡調用、比較耗時的處理程序,因為,事務中數據庫連接是不會釋放的,如果每個事務的處理時間都非常長,那么寶貴的數據庫連接資源將很快被耗盡。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM