轉載: 原文鏈接:https://www.jianshu.com/p/befc2d73e487
一、事務簡單介紹
事務指邏輯上的一組操作,組成這組操作的各個單元,要不全部成功,要不全部不成功。
1.1 事務基本要素
- 原子性(Atomicity): 事務開始后所有操作,要么全部做完,要么全部不做,不可能停滯在中間環節。事務執行過程中出錯,會回滾到事務開始前的狀態,所有的操作就像沒有發生一樣。也就是說事務是一個不可分割的整體,就像化學中學過的原子,是物質構成的基本單位。
- 一致性(Consistency): 事務開始前和結束后,數據庫的完整性約束沒有被破壞。比如A向B轉賬,不可能A扣了錢,B卻沒收到。
- 隔離性(Isolation): 同一時間,只允許一個事務請求同一數據,不同的事務之間彼此沒有任何干擾。比如A正在從一張銀行卡中取錢,在A取錢的過程結束前,B不能向這張卡轉賬。
- 持久性(Durability): 事務完成后,事務對數據庫的所有更新將被保存到數據庫,不能回滾。
1.2 Spring事務屬性
Spring事務屬性對應TransactionDefinition類里面的各個方法。TransactionDefinition類方法如下所示:(TransactionDefinition類介紹在文章最下面)
public interface TransactionDefinition { /** * 返回事務傳播行為 */ int getPropagationBehavior(); /** * 返回事務的隔離級別,事務管理器根據它來控制另外一個事務可以看到本事務內的哪些數據 */ int getIsolationLevel(); /** * 事務超時時間,事務必須在多少秒之內完成 */ int getTimeout(); /** * 事務是否只讀,事務管理器能夠根據這個返回值進行優化,確保事務是只讀的 */ boolean isReadOnly(); /** * 事務名字 */ @Nullable String getName(); }
事務屬性可以理解成事務的一些基本配置,描述了事務策略如何應用到方法上。事務屬性包含了5個方面:傳播行為、隔離規則、回滾規則、事務超時、是否只讀。
事務的產生需要依賴這些事務屬性。包括我們下面要講到的@Transactional注解的屬性其實就是在設置這些值。
1.2.1 傳播行為
當事務方法被另一個事務方法調用時,必須指定事務應該如何傳播。例如:方法可能繼續在現有事務中運行,也可能開啟一個新事務,並在自己的事務中運行。Spring定義了七種傳播行為:
1.2.2 隔離規則
隔離級別定義了一個事務可能受其他並發事務影響的程度。
在實際開發過程中,我們絕大部分的事務都是有並發情況。下多個事務並發運行,經常會操作相同的數據來完成各自的任務。在這種情況下可能會導致以下的問題:
- 臟讀(Dirty reads)—— 事務A讀取了事務B更新的數據,然后B回滾操作,那么A讀取到的數據是臟數據。
- 不可重復讀(Nonrepeatable read)—— 事務 A 多次讀取同一數據,事務 B 在事務A多次讀取的過程中,對數據作了更新並提交,導致事務A多次讀取同一數據時,結果不一致。
- 幻讀(Phantom read)—— 系統管理員A將數據庫中所有學生的成績從具體分數改為ABCDE等級,但是系統管理員B就在這個時候插入了一條具體分數的記錄,當系統管理員A改結束后發現還有一條記錄沒有改過來,就好像發生了幻覺一樣,這就叫幻讀。
不可重復讀的和幻讀很容易混淆,不可重復讀側重於修改,幻讀側重於新增或刪除。解決不可重復讀的問題只需鎖住滿足條件的行,解決幻讀需要鎖表
咱們已經知道了在並發狀態下可能產生: 臟讀、不可重復讀、幻讀的情況。因此我們需要將事務與事務之間隔離。根據隔離的方式來避免事務並發狀態下臟讀、不可重復讀、幻讀的產生。Spring中定義了五種隔離規則:
ISOLATION_SERIALIZABLE 隔離規則類型在開發中很少用到。舉個很簡單的例子。咱們使用了ISOLATION_SERIALIZABLE規則。A,B兩個事務操作同一個數據表並發過來了。A先執行。A事務這個時候會把表給鎖住,B事務執行的時候直接報錯。
補充:
- 事務隔離級別為ISOLATION_READ_UNCOMMITTED時,寫數據只會鎖住相應的行。
- 事務隔離級別為可ISOLATION_REPEATABLE_READ時,如果檢索條件有索引(包括主鍵索引)的時候,默認加鎖方式是next-key鎖;如果檢索條件沒有索引,更新數據時會鎖住整張表。一個間隙被事務加了鎖,其他事務是不能在這個間隙插入記錄的,這樣可以防止幻讀。
- 事務隔離級別為ISOLATION_SERIALIZABLE時,讀寫數據都會鎖住整張表。
- 隔離級別越高,越能保證數據的完整性和一致性,但是對並發性能的影響也就越大。
1.2.3 回滾規則
事務回滾規則定義了哪些異常會導致事務回滾而哪些不會。默認情況下,只有未檢查異常(RuntimeException和Error類型的異常)會導致事務回滾。而在遇到檢查型異常時不會回滾。 但是你可以聲明事務在遇到特定的檢查型異常時像遇到運行期異常那樣回滾。同樣,你還可以聲明事務遇到特定的異常不回滾,即使這些異常是運行期異常。
1.2.4 事務超時
為了使應用程序很好地運行,事務不能運行太長的時間。因為事務可能涉及對后端數據庫的鎖定,也會占用數據庫資源。事務超時就是事務的一個定時器,在特定時間內事務如果沒有執行完畢,那么就會自動回滾,而不是一直等待其結束。
1.2.5 是否只讀
如果在一個事務中所有關於數據庫的操作都是只讀的,也就是說,這些操作只讀取數據庫中的數據,而並不更新數據, 這個時候我們應該給該事務設置只讀屬性,這樣可以幫助數據庫引擎優化事務。提升效率。
二、@Transactional使用
Spring 為事務管理提供了豐富的功能支持。Spring 事務管理分為編碼式和聲明式的兩種方式:
-
編程式事務:允許用戶在代碼中精確定義事務的邊界。編程式事務管理使用TransactionTemplate或者直接使用底層的PlatformTransactionManager。對於編程式事務管理,spring推薦使用TransactionTemplate。
-
聲明式事務: 基於AOP,有助於用戶將操作與事務規則進行解耦。其本質是對方法前后進行攔截,然后在目標方法開始之前創建或者加入一個事務,在執行完目標方法之后根據執行情況提交或者回滾事務。聲明式事務管理也有兩種常用的方式,一種是在配置文件(xml)中做相關的事務規則聲明,另一種是基於@Transactional注解的方式。顯然基於注解的方式更簡單易用,更清爽。@Transactional注解的使用也是我們本文着重要理解的部分。
顯然聲明式事務管理要優於編程式事務管理,這正是spring倡導的非侵入式的開發方式。聲明式事務管理使業務代碼不受污染,一個普通的POJO對象,只要加上注解就可以獲得完全的事務支持。和編程式事務相比,聲明式事務唯一不足地方是,后者的最細粒度只能作用到方法級別,無法做到像編程式事務那樣可以作用到代碼塊級別。但是即便有這樣的需求,也存在很多變通的方法,比如,可以將需要進行事務管理的代碼塊獨立為方法等等。
2.1 @Transactional介紹
@Transactional注解 可以作用於接口、接口方法、類以及類方法上。當作用於類上時,該類的所有 public 方法將都具有該類型的事務屬性,同時,我們也可以在方法級別使用該標注來覆蓋類級別的定義。
雖然@Transactional 注解可以作用於接口、接口方法、類以及類方法上,但是 Spring 建議不要在接口或者接口方法上使用該注解,因為這只有在使用基於接口的代理時它才會生效。另外, @Transactional注解應該只被應用到 public 方法上,這是由Spring AOP的本質決定的。如果你在 protected、private 或者默認可見性的方法上使用 @Transactional 注解,這將被忽略,也不會拋出任何異常。
默認情況下,只有來自外部的方法調用才會被AOP代理捕獲,也就是,類內部方法調用本類內部的其他方法並不會引起事務行為,即使被調用方法使用@Transactional注解進行修飾。
2.2 @Transactional注解屬性
@Transactional注解里面的各個屬性和咱們在上面講的事務屬性里面是一一對應的。用來設置事務的傳播行為、隔離規則、回滾規則、事務超時、是否只讀。
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { /** * 當在配置文件中有多個 TransactionManager , 可以用該屬性指定選擇哪個事務管理器。 */ @AliasFor("transactionManager") String value() default ""; /** * 同上。 */ @AliasFor("value") String transactionManager() default ""; /** * 事務的傳播行為,默認值為 REQUIRED。 */ Propagation propagation() default Propagation.REQUIRED; /** * 事務的隔離規則,默認值采用 DEFAULT。 */ Isolation isolation() default Isolation.DEFAULT; /** * 事務超時時間。 */ int timeout() default TransactionDefinition.TIMEOUT_DEFAULT; /** * 是否只讀事務 */ boolean readOnly() default false; /** * 用於指定能夠觸發事務回滾的異常類型。 */ Class<? extends Throwable>[] rollbackFor() default {}; /** * 同上,指定類名。 */ String[] rollbackForClassName() default {}; /** * 用於指定不會觸發事務回滾的異常類型 */ Class<? extends Throwable>[] noRollbackFor() default {}; /** * 同上,指定類名 */ String[] noRollbackForClassName() default {}; }
2.2.1 value、transactionManager屬性
它們兩個是一樣的意思。當配置了多個事務管理器時,可以使用該屬性指定選擇哪個事務管理器。大多數項目只需要一個事務管理器。然而,有些項目為了提高效率、或者有多個完全不同又不相干的數據源,從而使用了多個事務管理器。機智的Spring的Transactional管理已經考慮到了這一點,首先定義多個transactional manager,並為qualifier屬性指定不同的值;然后在需要使用@Transactional注解的時候指定TransactionManager的qualifier屬性值或者直接使用bean名稱。配置和代碼使用的例子:
<tx:annotation-driven/> <bean id="transactionManager1" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="datasource1"></property> <qualifier value="datasource1Tx"/> </bean> <bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="datasource2"></property> <qualifier value="datasource2Tx"/> </bean>
public class TransactionalService { @Transactional("datasource1Tx") public void setSomethingInDatasource1() { ... } @Transactional("datasource2Tx") public void doSomethingInDatasource2() { ... } }
2.2.2 propagation屬性
propagation用於指定事務的傳播行為,默認值為 REQUIRED。propagation有七種類型,就是我們在上文中講到的事務屬性傳播行為的七種方式,如下所示:
2.2.3 isolation屬性
isolation用於指定事務的隔離規則,默認值為DEFAULT。@Transactional的隔離規則和上文事務屬性里面的隔離規則也是一一對應的。總共五種隔離規則,如下所示:
2.2.4 timeout
timeout用於設置事務的超時屬性。
2.2.5 readOnly
readOnly用於設置事務是否只讀屬性。
2.2.6 rollbackFor、rollbackForClassName、noRollbackFor、noRollbackForClassName
rollbackFor、rollbackForClassName用於設置那些異常需要回滾;noRollbackFor、noRollbackForClassName用於設置那些異常不需要回滾。他們就是在設置事務的回滾規則。
2.3 @Transactional注解的使用
@Transactional注解的使用關鍵點在理解@Transactional注解里面各個參數的含義。這個咱們在上面已經對@Transactional注解參數的各個含義做了一個簡單的介紹。接下來,咱們着重講一講@Transactional注解使用過程中一些注意的點。
@Transactional注解內部實現依賴於Spring AOP編程。而AOP在默認情況下,只有來自外部的方法調用才會被AOP代理捕獲,也就是,類內部方法調用本類內部的其他方法並不會引起事務行為。
2.3.1 @Transactional 注解盡量直接加在方法上
為什么:因為@Transactional直接加在類或者接口上,@Transactional注解會對類或者接口里面所有的public方法都有效(相當於所有的public方法都加上了@Transactional注解,而且注解帶的參數都是一樣的)。第一影響性能,可能有些方法我不需要@Transactional注解,第二方法不同可能@Transactional注解需要配置的參數也不同,比如有一個方法只是做查詢操作,那咱們可能需要配置Transactional注解的readOnly參數。所以強烈建議@Transactional注解直接添加的需要的方法上。
2.3.2 @Transactional 注解必須添加在public方法上,private、protected方法上是無效的
在使用@Transactional 的時候一定要記住,在private,protected方法上添加@Transactional 注解不會有任何效果。相當於沒加一樣。即使外部能調到protected的方法也無效。和沒有添加@Transactional一樣。
2.3.3 函數之間相互調用
關於有@Transactional的函數之間調用,會產生什么情況。這里咱們通過幾個例子來說明。
2.3.3.1 同一個類中函數相互調用
同一個類AClass中,有兩個函數aFunction、aInnerFunction。aFunction調用aInnerFunction。而且aFunction函數會被外部調用。
情況0: aFunction添加了@Transactional注解,aInnerFunction函數沒有添加。aInnerFunction拋異常。
public class AClass { @Transactional(rollbackFor = Exception.class) public void aFunction() { //todo: 數據庫操作A(增,刪,該) aInnerFunction(); // 調用內部沒有添加@Transactional注解的函數 } private void aInnerFunction() { //todo: 操作數據B(做了增,刪,改 操作) throw new RuntimeException("函數執行有異常!"); } }
結果:兩個函數操作的數據都會回滾。
情況1:兩個函數都添加了@Transactional注解。aInnerFunction拋異常。
public class AClass { @Transactional(rollbackFor = Exception.class) public void aFunction() { //todo: 數據庫操作A(增,刪,該) aInnerFunction(); // 調用內部沒有添加@Transactional注解的函數 } @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) private void aInnerFunction() { //todo: 操作數據B(做了增,刪,改 操作) throw new RuntimeException("函數執行有異常!"); } }
結果:同第一種情況一樣,兩個函數對數據庫操作都會回滾。因為同一個類中函數相互調用的時候,內部函數添加@Transactional注解無效。@Transactional注解只有外部調用才有效。
情況2: aFunction不添加注解,aInnerFunction添加注解。aInnerFunction拋異常。
public class AClass { public void aFunction() { //todo: 數據庫操作A(增,刪,該) aInnerFunction(); // 調用內部沒有添加@Transactional注解的函數 } @Transactional(rollbackFor = Exception.class) protected void aInnerFunction() { //todo: 操作數據B(做了增,刪,改 操作) throw new RuntimeException("函數執行有異常!"); } }
結果:兩個函數對數據庫的操作都不會回滾。因為內部函數@Transactional注解添加和沒添加一樣。
情況3:aFunction添加了@Transactional注解,aInnerFunction函數沒有添加。aInnerFunction拋異常,不過在aFunction里面把異常抓出來了。
public class AClass { @Transactional(rollbackFor = Exception.class) public void aFunction() { //todo: 數據庫操作A(增,刪,該) try { aInnerFunction(); // 調用內部沒有添加@Transactional注解的函數 } catch (Exception e) { e.printStackTrace(); } } private void aInnerFunction() { //todo: 操作數據B(做了增,刪,改 操作) throw new RuntimeException("函數執行有異常!"); } }
結果:兩個函數里面的數據庫操作都成功。事務回滾的動作發生在當有@Transactional注解函數有對應異常拋出時才會回滾。(當然了要看你添加的@Transactional注解有沒有效)。
2.3.3.1. 不同類中函數相互調用
兩個類AClass、BClass。AClass類有aFunction、BClass類有bFunction。AClass類aFunction調用BClass類bFunction。最終會在外部調用AClass類的aFunction。
情況0:aFunction添加注解,bFunction不添加注解。bFunction拋異常。
@Service() public class AClass { private BClass bClass; @Autowired public void setbClass(BClass bClass) { this.bClass = bClass; } @Transactional(rollbackFor = Exception.class) public void aFunction() { //todo: 數據庫操作A(增,刪,該) bClass.bFunction(); } } @Service() public class BClass { public void bFunction() { //todo: 數據庫操作A(增,刪,該) throw new RuntimeException("函數執行有異常!"); } }
結果:兩個函數對數據庫的操作都回滾了。
情況1:aFunction、bFunction兩個函數都添加注解,bFunction拋異常。
@Service() public class AClass { private BClass bClass; @Autowired public void setbClass(BClass bClass) { this.bClass = bClass; } @Transactional(rollbackFor = Exception.class) public void aFunction() { //todo: 數據庫操作A(增,刪,該) bClass.bFunction(); } } @Service() public class BClass { @Transactional(rollbackFor = Exception.class) public void bFunction() { //todo: 數據庫操作A(增,刪,該) throw new RuntimeException("函數執行有異常!"); } }
結果:兩個函數對數據庫的操作都回滾了。兩個函數里面用的還是同一個事務。這種情況下,你可以認為事務rollback了兩次。兩個函數都有異常。
情況2:aFunction、bFunction兩個函數都添加注解,bFunction拋異常。aFunction抓出異常。
@Service() public class AClass { private BClass bClass; @Autowired public void setbClass(BClass bClass) { this.bClass = bClass; } @Transactional(rollbackFor = Exception.class) public void aFunction() { //todo: 數據庫操作A(增,刪,該) try { bClass.bFunction(); } catch (Exception e) { e.printStackTrace(); } } } @Service() public class BClass { @Transactional(rollbackFor = Exception.class) public void bFunction() { //todo: 數據庫操作A(增,刪,該) throw new RuntimeException("函數執行有異常!"); } }
結果:兩個函數數據庫操作都沒成功。而且還拋異常了。org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only。看打印出來的解釋也很好理解把。咱們也可以這么理解,兩個函數用的是同一個事務。bFunction函數拋了異常,調了事務的rollback函數。事務被標記了只能rollback了。程序繼續執行,aFunction函數里面把異常給抓出來了,這個時候aFunction函數沒有拋出異常,既然你沒有異常那事務就需要提交,會調事務的commit函數。而之前已經標記了事務只能rollback-only(以為是同一個事務)。直接就拋異常了,不讓調了。
情況3:aFunction、bFunction兩個函數都添加注解,bFunction拋異常。aFunction抓出異常。這里要注意bFunction函數@Transactional注解我們是有變化的,加了一個參數propagation = Propagation.REQUIRES_NEW,控制事務的傳播行為。表明是一個新的事務。其實咱們情況3就是來解決情況2的問題的。
@Service() public class AClass { private BClass bClass; @Autowired public void setbClass(BClass bClass) { this.bClass = bClass; } @Transactional(rollbackFor = Exception.class) public void aFunction() { //todo: 數據庫操作A(增,刪,該) try { bClass.bFunction(); } catch (Exception e) { e.printStackTrace(); } } } @Service() public class BClass { @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void bFunction() { //todo: 數據庫操作A(增,刪,該) throw new RuntimeException("函數執行有異常!"); } }
結果:bFunction函數里面的操作回滾了,aFunction里面的操作成功了。有了前面情況2的理解。這種情況也很好解釋。兩個函數不是同一個事務了。
關於@Transactional注解的使用,就說這么些。最后做幾點總結:
-
要知道@Transactional注解里面每個屬性的含義。@Transactional注解屬性就是來控制事務屬性的。通過這些屬性來生成事務。
-
要明確我們添加的@Transactional注解會不會起作用。@Transactional注解在外部調用的函數上才有效果,內部調用的函數添加無效,要切記。這是由AOP的特性決定的。
-
要明確事務的作用范圍,有@Transactional的函數調用有@Transactional的函數的時候,進入第二個函數的時候是新的事務,還是沿用之前的事務。稍不注意就會拋UnexpectedRollbackException異常。
Spring的TransactionDefinition接口介紹
TransactionDefinition類結構

作用
1.TransactionDefinition接口被用於Spring事物支持的核心PlatformTransactionManager接口,該接口實現在特定平台(如JDBC、JTA)上執行事務管理;
2.其核心PlatformTransactionManager.getTransaction()方法將TransactionDefinition接口作為參數,並返回TransactionStatus接口;
3.TransactionStatus接口用於控制事務執行,即設置事務結果、檢查事務是否完成或是否為新事務;