事務(Transaction)是并發控制的單位,是用戶定義的一個操作序列。這些操作要么都做,要么都不做,是一個不可分割的工作單位
事務的隔離級別
臟讀:事務1更新數據,未提交時,事務2讀取,事務2讀取的是事務1修改后的值,此時事務1出錯回滾,導致事務2臟讀;
不可重復讀:事務1多次查詢,查詢1與查詢2期間,事務2修改了數據并提交,事務1中查詢1與查詢2獲取的數據不一致
幻讀:事務1多次查詢,查詢1與查詢2期間,事務2新增了數據,事務1中查詢1與查詢2獲取的數據不一致
- READ UNCOMMITTED(讀未提交): 事務執行時可以看到其他事務“沒有提交的新增+修改”記錄。可以讀到一個事務的中間過程,違背了事務的隔離性,基本不會使用。
結果:導致臟讀,導致不可重復讀,導致幻讀- READ COMMITTED (讀已提交) : 事務執行時可以看到其他事務“已經提交的新增+修改”記錄。
結果:避免臟讀,導致不可重復讀,可能導致幻讀- REPEATABLE READ(可重復讀): 事務執行時可以看到其他事務“已經提交的新增”記錄。
結果:避免臟讀,避免不可重復讀,可能導致幻讀(mvcc機制優化)(了解間隙鎖,也可解決幻讀)- SERIALIZABLE (串行化): 事務執行時看不到其他事務對數據庫所做的更新。兩個事務實際上是串行化方式運行。
結果:避免臟讀,避免不可重復讀,避免幻讀
Spring隔離界別體現
@Transactional(isolation = Isolation.DEFAULT)
Isolation.DEFAULT 即默認與數據庫一致
查看mysql數據庫隔離級別
SELECT @@GLOBAL.transaction_isolation, @@transaction_isolation;
Mysql默認的事務隔離級別是REPEATABLE-READ。
SERIALIAZABLE如何保證隔離級別
SERIALIAZABLE事務隔離級別采用使用 2PL(Two-Phase Locking,兩階段鎖)機制來控制本地事務的并發,保證隔離性。2PL 是將鎖操作分為加鎖和解鎖兩個階段,并且保證兩個階段完全不相交。加鎖階段,只加鎖,不放鎖。解鎖階段,只放鎖,不加鎖。在一個本地事務中,每執行一條更新操作之前,都會先獲取對應的鎖資源,只有獲取鎖資源成功才會執行該操作,并且一旦獲取了鎖資源就會持有該鎖資源直到本事務執行結束。MySQL 通過這種 2PL 機制,可以保證在本地事務執行過程中,其他并發事務不能操作相同資源,從而實現了事務隔離。由于讀寫都需要上鎖,性能差。
MVCC(Multi Version Concurrency Control,多版本并發控制)
mvcc查用快照版本,增刪改用最新版本,超賣等場景解決方案之一
MySql通過內部機制避免幻讀,在rr隔離級別下幾乎達到了串行化隔離級別的效果。
InnoDB是在undolog中實現的MVCC,通過undolog可以找回數據的歷史版本。
MVCC只在 READ COMMITTED 和 REPEATABLE READ 兩個隔離級別下工作。其他兩個隔離級別和MVCC不兼容, 因為 READ UNCOMMITTED 總是讀取最新的數據行, 而不是符合當前事務版本的數據行。而 SERIALIZABLE 則會對所有讀取的行都加鎖。
InnoDB存儲引擎在數據庫每行數據的后面添加了三個字段
事務ID(DB_TRX_ID)字段: 用來標識最近一次對本行記錄做修改(insert|update)的事務的標識符, 即最后一次修改(insert|update)本行記錄的事務id。
回滾指針(DB_ROLL_PTR)字段: 指寫入回滾段(rollback segment)的 undo log record (撤銷日志記錄)。
DB_ROW_ID字段: 表中沒有主鍵或合適的唯一索引, 也就是無法生成聚簇索引的時候, InnoDB會幫我們自動生成聚集索引, 聚簇索引會使用DB_ROW_ID的值來作為主鍵; 如果我們有自己的主鍵或者合適的唯一索引, 那么聚簇索引中也就不會包含 DB_ROW_ID 了。
對于刪除操作,mysql底層會記錄好被刪除的數據行的回滾事務id,對于更新操作,mysql底層會新增一條相同數據并記錄好對應的事務id。
在RR級別下,快照讀(簡單的select操作)是通過MVVC和undo log來實現的
當前讀(select ... lock in share mode,select ... for update,insert,update,delete)是通過加record lock(記錄鎖)和gap lock(間隙鎖)來實現的。
事務的傳播行為
1:PROPAGATION_REQUIRED
如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,則加入該事務。
比如說,ServiceB.methodB的事務級別定義為PROPAGATION_REQUIRED, 那么由于執行ServiceA.methodA的時候,ServiceA.methodA已經起了事務,這時調用ServiceB.methodB,ServiceB.methodB看到自己已經運行在ServiceA.methodA的事務內部,就不再起新的事務。而假如ServiceA.methodA運行的時候發現自己沒有在事務中,他就會為自己分配一個事務。這樣,在ServiceA.methodA或者在ServiceB.methodB內的任何地方出現異常,事務都會被回滾。即使ServiceB.methodB的事務已經被提交,但是ServiceA.methodA在接下來fail要回滾,ServiceB.methodB也要回滾。
2:PROPAGATION_SUPPORTS
如果當前在事務中,即以事務的形式運行,如果當前不再一個事務中,那么就以非事務的形式運行
3:PROPAGATION_MANDATORY
必須在一個事務中運行,否則就要拋出異常。
4:PROPAGATION_REQUIRES_NEW
新建事務,如果當前事務存在,則把當前事務掛起
比如我們設計ServiceA.methodA的事務級別為PROPAGATION_REQUIRED,ServiceB.methodB的事務級別為PROPAGATION_REQUIRES_NEW,那么當執行到ServiceB.methodB的時候,ServiceA.methodA所在的事務就會掛起,ServiceB.methodB會起一個新的事務,等待ServiceB.methodB的事務完成以后,他才繼續執行。他與PROPAGATION_REQUIRED 的事務區別在于事務的回滾程度了。因為ServiceB.methodB是新起一個事務,那么就是存在兩個不同的事務。如果ServiceB.methodB已經提交,那么ServiceA.methodA失敗回滾,ServiceB.methodB是不會回滾的。如果ServiceB.methodB失敗回滾,如果他拋出的異常被ServiceA.methodA捕獲,ServiceA.methodA事務仍然可能提交。
5: PROPAGATION_NOT_SUPPORTED
以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
比如ServiceA.methodA的事務級別是PROPAGATION_REQUIRED ,而ServiceB.methodB的事務級別是PROPAGATION_NOT_SUPPORTED ,那么當執行到ServiceB.methodB時,ServiceA.methodA的事務掛起,而他以非事務的狀態運行完,再繼續ServiceA.methodA的事務。
6:PROPAGATION_NEVER
以非事務方式執行操作,如果當前存在事務,則拋出異常。
假設ServiceA.methodA的事務級別是PROPAGATION_REQUIRED, 而ServiceB.methodB的事務級別是PROPAGATION_NEVER ,那么ServiceB.methodB就要拋出異常了。
7:PROPAGATION_NESTED
理解Nested的關鍵是savepoint。他與PROPAGATION_REQUIRES_NEW的區別是,PROPAGATION_REQUIRES_NEW另起一個事務,將會與他的父事務相互獨立,而Nested的事務和他的父事務是相依的,他的提交是要等和他的父事務一塊提交的。也就是說,如果父事務最后回滾,他也要回滾的。而Nested事務的好處是他有一個savepoint。
解決Transactional注解不回滾
1、檢查你方法是不是public的,不能作用在方法的內部的任何方法上
2、你的異常類型是不是unchecked異常,即運行時異常 (java里面將派生于Error或者RuntimeException(比如空指針,數組越界,1/0)的異常稱為unchecked異常,其他繼承自java.lang.Exception得異常統稱為Checked Exception,如IOException、TimeoutException等)。如果想check異常也想回滾怎么辦,注解上面寫明異常類型即可@Transactional(rollbackFor=Exception.class)。類似的還有norollbackFor,自定義不回滾的異常
3、數據庫引擎要支持事務,如果是MySQL,注意表要使用支持事務的引擎,比如innodb,如果是myisam,事務是不起作用的
4、是否開啟了對注解的解析(Springboot不需關注,Springboot會自動配置,5同)
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
5、spring是否掃描到你這個包,如下是掃描到org.test下面的包
<context:component-scan base-package="org.test" ></context:component-scan>
6、檢查是不是同一個類中的方法調用(如a方法調用同一個類中的b方法)
7、異常是不是被你catch住了
解決不能作用在方法的任何內部方法上
1.注入本身service,在當前處理service注入自己,然后內部調用 推薦
2.(1).在未定義事務的方法調用帶有事務的方法使用AopContext.currentProxy()((AfterSaleManager)AopContext.currentProxy()).innerRefund(goodsReturnsApply); 可用
(2).spring配置文件配置
<aop:aspectj-autoproxy expose-proxy="true"/>
3、重新定義對象 applicationContext.getBean() 未測試
4、使用@Aspect注解 未測試
希望在編譯期間進行織入(weaving),還是編譯后(post-compile)或是運行時(run-time)。Spring只支持運行時織入,你可以選擇使用AspectJ編譯后(post-compile)或載入時(load-time)織入。Spring基于代理模式(使用CGLIB),它有一個使用限制,即無法在使用final修飾的bean上應用橫切關注點。
借鑒:https://juejin.im/post/5b90cbf4e51d450e84776d27