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。但是我們基本不推薦使用編程式事務。下圖展示的是編程式事務的實現,完全有程序員來實現。
[2]、聲明式事務管理
聲明式事務管理:事務的控制交給Spring框架來管理,開發人員只需要在Spring框架的配置文件中聲明你需要的功能即可。
Spring中聲明式事務管理的底層是基於AOP來完成的,其本質是對方法前后進行攔截,然后在目標方法開始之前創建或者加入一個事務,執行完目標方法之后根據執行的情況提交或者回滾。聲明式事務它將具體業務與事務處理部分解耦,代碼侵入性很低,所以在實際開發中聲明式事務用的比較多。
[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接口中:
事務的傳播行為如下表所示(主要學習前兩個即可,其它的簡單了解):
事務傳播行為
|
描述 |
---|---|
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():
②、創建一個PropagationServiceImpl類
③、junit測試代碼:
④、測試結論:
測試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,可以讓事物在遇到非運行時異常時也回滾。
設置方式如下所示(實際開發時通常也建議設置為根據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>
注意事項:
- 雖然切入點表達式已經定位到了所有需要事務的方法,但是在tx:attributes中還是必須配置事務屬性。這兩個條件缺一不可。缺少任何一個條件,方法都加不上事務。
- 另外,tx:advice導入時需要注意名稱空間的值,不要導錯了,因為導錯了很難發現。
參考鏈接: