Spring中對事務的支持


1、事務的回顧

[1]、什么是事務?

事務就是由一組SQL組成的單元,該單元要么整體執行成功,要么整體執行失敗。


[2]、事務的ACID屬性

  • 原子性(Atomicity):指事務中包含所操作的SQL是一個不可分割的工作單位,要么都執行成功,要么都執行失敗,其中只要有一條SQL出現錯誤都會回滾到原來的狀態。
  • 一致性(Consistency):事務的執行不能破壞數據庫數據的完整性和一致性,一個事務在執行之前和執行之后,數據庫都必須處於一致性狀態。比如A和B兩者的錢加起來一共是1000,那么不管A和B之間如何轉賬、轉幾次賬,事務結束后兩個用戶的錢相加起來應該還得是1000,並且在當前事務中,A減了多少錢,B加了多錢這個中間狀態是不可見的,這就是事務的一致性。
  • 隔離性(Isolation):一個事務所做的修改在最終提交以前,對其他事務是不可見的。即一個事務內部的操作及使用的數據對並發的其他事務是隔離的,並發執行的各個事務之間不能互相干擾。比如A正在從一張銀行卡中取錢,在A取錢結束前,B不能向這張卡轉賬。
  • 持久性(Durability):指的是一個事務一旦被提交,數據就被永遠的存儲到磁盤上了,即使系統發生故障,數據仍然不會丟失。

[3]、事務執行過程中的並發問題

  • 臟讀:事務A讀取了事務B更新並且未提交的數據,然后B回滾操作,那么A讀取到的數據是臟數據
    • 初始狀態:數據庫中age字段數據的值是20
    • T1把age修改為了30
    • T2讀取了age現在的值:30
    • T1回滾了自己的操作,age恢復為了原來的20
    • 此時T2讀取到的30就是一個不存在的“臟”的數據
  • 不可重復讀:事務 A 多次讀取同一數據,事務 B 在事務A多次讀取的過程中,對數據作了更新並提交,導致事務A多次讀取同一數據時,結果不一致。
    • T1第一次讀取age是20
    • T2修改age為30並提交事務,此時age確定修改為了30
    • T1第二次讀取age得到的是30
  • 幻讀:事務A從一個表中讀取了一個字段,然后B在該表中插入/刪除了一些新的行。 之后, 如果 A 再次讀取同一個表, 就會多/少幾行,就好像發生了幻覺一樣,這就叫幻讀。
    • T1第一次執行count(*)返回500
    • T2執行了insert操作
    • T1第二次執行count(*)返回501,感覺像是出現了幻覺

補充:不可重復讀的和幻讀很容易混淆,不可重復讀側重於修改,幻讀側重於新增或刪除。解決不可重復讀的問題只需鎖住滿足條件的行,解決幻讀需要鎖表


[4]、事務的隔離級別

SQL標准定義了4種隔離級別(從低到高),分別對應可能出現的數據不一致的情況:

事務隔離級別 臟讀 不可重復讀 幻讀
讀未提交(read-uncommitted)
讀已提交(read-committed)
可重復讀(repeatable-read)
串行化(serializable)

4種隔離級別的描述:

  • 讀未提交(read-uncommitted):允許A事務讀取其他事務未提交和已提交的數據
  • 讀已提交(read-committed):只允許A事務讀取其他事務已提交的數據
  • 可重復讀(repeatable-read):確保事務可以多次從一個字段中讀取相同的值。在這個事務持續期間,禁止其他事務對這個字段進行更新;注意:mysql中使用了MVCC多版本控制技術,在這個級別也可以避免幻讀。
  • 串行化(serializable):鎖定整個表,讓對整個表的操作全部排隊串行執行。能解決所有並發問題,安全性最好,但是性能極差,基本不用。

2、Spring中的事務介紹

Spring框架中對事務的支持有兩種:

  • 編程式事務管理
  • 聲明式事務管理(推薦)

[1]、編程式事務管理

編程式事務管理:事務的相關操作完全由開發人員通過編碼實現。所以編程式事務管理是侵入性事務管理,使用TransactionTemplate或者直接使用PlatformTransactionManager,對於編程式事務管理,Spring推薦使用TransactionTemplate。但是我們基本不推薦使用編程式事務。下圖展示的是編程式事務的實現,完全有程序員來實現。

image


[2]、聲明式事務管理

聲明式事務管理:事務的控制交給Spring框架來管理,開發人員只需要在Spring框架的配置文件中聲明你需要的功能即可。

Spring中聲明式事務管理的底層是基於AOP來完成的,其本質是對方法前后進行攔截,然后在目標方法開始之前創建或者加入一個事務,執行完目標方法之后根據執行的情況提交或者回滾。聲明式事務它將具體業務與事務處理部分解耦,代碼侵入性很低,所以在實際開發中聲明式事務用的比較多。

image


[3]、Spring事務的相關接口

Spring事務管理的相關接口有三個,如下:

  • PlatformTransactionManager:事務管理器,為不同的數據訪問技術的事務提供不同的接口實現
  • TransactionDefinition: 事務定義信息(事務隔離級別、傳播行為、超時、只讀、回滾規則)
  • TransactionStatus: 事務的運行狀態

Spring的事務機制是用統一的機制來處理不同數據訪問技術的事務處理,Spring並不直接管理事務,而是提供了多種事務管理器。Spring的事務機制提供了一個org.springframework.transaction.PlatformTransactionManager接口,將事務管理的職責委托給JDBC或者Hibernate等持久化機制所提供的相關平台框架的事務來實現。通過這個接口,Spring為各個平台如JDBC、Hibernate等都提供了對應的事務管理器,其具體的實現就是各個平台自己的事情了,對應的相關實現如下表所示。

數據庫訪問技術 實現
JDBC DataSourceTransactionManager
JPA JpaTransactionManager
Hibernate HibernateJpaTransactionManager
JDO JdoTransactionManager
分布式事務 JtaTransactionManager

3、基於注解的聲明式事務

在Spring中使用聲明式事務一般會使用注解來實現,即@Transactional注解,該注解可以使用在類、接口和方法上:

  • 作用在類:表示所有該類的 public 方法都配置相同的事務屬性信息。
  • 作用在方法:當類配置了@Transactional,方法也配置了@Transactional,方法的事務會覆蓋類的事務配置信息。
  • 作用於接口:不推薦這種使用方法,因為一旦標注在Interface上並且配置了Spring AOP 使用CGLib動態代理,將會導致@Transactional注解失效。
@Transactional
public class Trans {
    @Transactional
    public void saveSomething() {
        //...相關操作 
    }
}

需要特別注意的是,此@Transactional注解來自org.springframework.transaction.annotation包,而不是javax.transaction。

@Transactional注解中常用參數:

  • value:當在配置文件中有多個 TransactionManager,可以用該屬性指定選擇哪個事務管理器。
  • propagation:事務的傳播行為,默認值為 REQUIRED
  • isolation:事務的隔離級別,默認值為 DEFAULT,即采用數據庫的默認隔離級別。
  • timeout:事務的超時時間(單位是秒),默認值為 -1。如果超過該時間限制但事務還未提交,則自動回滾事務。
  • readOnly:用於指定事務是否為只讀事務,默認值為 false。為了忽略那些不需要事務的方法,比如select讀取數據,可以設置readOnly = true。
  • rollbackFor:指定能夠觸發事務回滾的異常類型,可以指定多個異常類型。
  • noRollbackFor:指定不用回滾事務的異常類型,可以指定多個異常類型。

下面是@Transactional注解的簡單使用(定義的是異常類是class對象):

@Transactional(
    propagation = Propagation.REQUIRED, // 傳播行為
    isolation = Isolation.DEFAULT,  // 隔離級別
    timeout = 1000,  // 事務的超時時間(單位是秒)
    readOnly = true,  // 事務是否為只讀
    rollbackFor = Exception.class,  // 能夠觸發事務回滾的異常類型
    noRollbackFor = Exception.class  // 不用回滾事務的異常類型
)
public void doSomething() {  
  //...相關操作
}

注意:在Spring中使用事務還需要在xml配置文件中配置如下內容:

    <!-- 1.配置事務管理器的bean -->
    <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
        <!-- 給事務管理器裝配數據源 -->
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- 2.開啟基於注解的聲明式事務 -->
    <!-- 在transaction-manager屬性中指定前面配置的事務管理器的bean的id -->
    <!-- transaction-manager屬性的默認值是transactionManager,如果正好前面bean的id就是這個默認值,那么transaction-manager屬性可以省略不配 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    <!-- 3.配置自動掃描的包 -->
    <context:component-scan base-package="com.thr.service"/>

4、事務的傳播機制(行為)

事務的傳播機制一般用在事務的嵌套中,當事務方法被另一個事務方法調用時,則應該指定事務如何傳播。比如事務方法A直接或間接調用了方法B,那么這兩個方法是各自作為獨立的方法提交,還是內層的事務合並到外層的事務一起提交,這就是需要事務傳播機制的配置來確定怎么樣執行。

注:事務的傳播行為和隔離級別都定義在TransactionDefinition接口中:

image

事務的傳播行為如下表所示(主要學習前兩個即可,其它的簡單了解):

事務傳播行為
描述
PROPAGATION_REQUIRED 支持外層事務。這是Spring默認的傳播機制,能滿足絕大部分業務需求,如果外層有事務,則當前事務加入到外層事務,一塊提交,一塊回滾。如果外層沒有事務,則創建一個新的事務。
PROPAGATION_REQUIRES_NEW 不支持外層事務。該事務傳播機制是每次都會新開啟一個事務,同時把外層事務掛起,當前事務執行完畢,恢復上層事務的執行。如果外層沒有事務,執行當前新開啟的事務即可。
PROPAGATION_SUPPORTS 支持外層事務。如果外層有事務,則加入外層事務,如果外層沒有事務,則直接使用非事務方式執行。完全依賴外層的事務
PROPAGATION_NOT_SUPPORTED 不支持外層事務。該傳播機制不支持事務,如果外層存在事務則掛起,執行完當前代碼,則恢復外層事務,無論是否異常都不會回滾當前的代碼
PROPAGATION_NEVER 不支持外層事務。該傳播機制不支持外層事務,即如果外層有事務就拋出異常
PROPAGATION_MANDATORY 支持外層事務。與NEVER相反,如果外層沒有事務,則拋出異常
PROPAGATION_NESTED Spring 所特有的。該傳播機制的特點是可以保存狀態保存點,當前事務回滾到某一個點,從而避免所有的嵌套事務都回滾,即各自回滾各自的,如果子事務沒有把異常吃掉,基本還是會引起全部回滾,等價於TransactionDefinition.PROPAGATION_REQUIRED。

簡單測試REQUIRED和REQUIRES_NEW兩種傳播行為:

①、在EmployeeServiceImpl中增加了兩個方法:updateOne()和updateTwo():

image

②、創建一個PropagationServiceImpl類

image

③、junit測試代碼:

image

④、測試結論:

測試REQUIRED:兩個方法的操作都沒有生效,updateTwo()方法回滾,導致updateOne()也一起被回滾,因為他們都在propagationService.update()方法開啟的同一個事務內。


測試REQUIRES_NEW:把updateOne()和updateTwo()這兩個方法上都使用下面的設置:

@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)

結果:

  • updateOne()沒有受影響,成功實現了更新
  • updateTwo()自己回滾

原因:上面兩個方法各自運行在自己的事務中。

5、事務的隔離級別

事務的隔離級別定義了一個事務可能受其他並發事務影響的程度。隔離級別可以不同程度的解決臟讀、不可重復讀、幻讀。

  • ISOLATION_DEFAULT:使用后端數據庫默認的隔離級別,Mysql 默認采用的 REPEATABLE_READ隔離級別,Oracle 默認采用的 READ_COMMITTED隔離級別。
  • ISOLATION_READ_UNCOMMITTED:不可提交讀,允許讀取尚未提交事務的數據,可能會導致臟讀、不可重復讀、幻讀。
  • ISOLATION_READ_COMMITTED:讀已提交,讀取並發事務已經提交的數據,可以阻止臟讀,但是幻讀或不可重復讀仍有可能發生。
  • ISOLATION_REPEATABLE_READ:可重復讀,可以阻止臟讀和不可重復讀,但幻讀仍有可能發生。
  • ISOLATION_SERIALIZABLE:串行化,這種級別是最高級別,完全服從ACID的隔離級別,確保阻止臟讀、不可重復讀以及幻讀。所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾。但是嚴重影響程序的性能。幾乎不會用到該級別。

6、只讀屬性和超時屬性

①、只讀屬性

一個事務如果是做查詢操作,可以設置為只讀,此時數據庫可以針對查詢操作來做優化,有利於提高性能。

@Transactional(readOnly = true)
public void doSomething() {  
  //...相關操作
}

如果是針對增刪改方法設置只讀屬性,則會拋出下面異常:

表面的異常信息:TransientDataAccessResourceException: PreparedStatementCallback
    
根本原因:SQLException: Connection is read-only. Queries leading to data modification are not allowed(連接是只讀的。查詢導向數據的修改是不允許的。)

實際開發時建議把查詢操作設置為只讀。

②、超時屬性

一個數據庫操作有可能因為網絡或死鎖等問題卡住很長時間,從而導致數據庫連接等資源一直處於被占用的狀態。所以我們可以設置一個超時屬性,讓一個事務執行太長時間后,主動回滾。事務結束后把資源釋放出來。

@Transactional(timeout = 60) //單位為秒
public void doSomething() {  
  //...相關操作
}

7、事務回滾的異常

在@Transactional注解中如果不配置rollbackFor屬性,那么事物只會在遇到RuntimeException的時候才會回滾,加上rollbackFor=Exception.class,可以讓事物在遇到非運行時異常時也回滾。

image

設置方式如下所示(實際開發時通常也建議設置為根據Exception異常回滾):

@Transactional(
    propagation = Propagation.REQUIRED, // 傳播行為
    isolation = Isolation.DEFAULT,  // 隔離級別
    timeout = 3000,  // 事務的超時時間
    readOnly = true,  // 事務是否為只讀
    rollbackFor = Exception.class,  // 能夠觸發事務回滾的異常類型
)
public void doSomething() {  
  //...相關操作
}

8、基於XML的聲明式事務

基於XML的方式配置聲明式事務也比較的簡單,其配置的方式如下所示:

    <!-- 配置基於XML的聲明式事務 -->
    <aop:config>
        <!-- 配置事務切面的切入點表達式 -->
        <aop:pointcut id="txPointCut" expression="execution(* *..*Service.*(..))"/>
        <!-- 將切入點表達式和事務通知關聯起來 -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
    </aop:config>
    <!-- 配置事務通知:包括對事務管理器的關聯,還有事務屬性 -->
    <!-- 如果事務管理器的bean的id正好是transactionManager,則transaction-manager屬性可以省略 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!-- 給具體的事務方法配置事務屬性 -->
        <tx:attributes>
            <!-- 指定具體的事務方法 -->
            <tx:method name="get*" read-only="true"/>
            <tx:method name="query*" read-only="true"/>
            <tx:method name="count*" read-only="true"/>
            <!-- 增刪改方法 -->
            <tx:method name="update*" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
            <tx:method name="insert*" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
            <tx:method name="delete*" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
        </tx:attributes>
    </tx:advice>

注意事項:

  1. 雖然切入點表達式已經定位到了所有需要事務的方法,但是在tx:attributes中還是必須配置事務屬性。這兩個條件缺一不可。缺少任何一個條件,方法都加不上事務。
  2. 另外,tx:advice導入時需要注意名稱空間的值,不要導錯了,因為導錯了很難發現。

image


參考鏈接:


免責聲明!

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



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