22.Spring事務總結
主要內容:併發問題,事務隔離級別,事務傳播,事務超時,只讀事務,異常處理
22.1 併發問題
一個數據庫可以允許多個客戶端同時訪問,即併發的方式訪問數據庫。數據庫中的同一個數據可能同時被多個事務訪問,如果沒有采取必要的隔離措施,就會導致各種併發問題,從而破壞數據的完整性。這些問題可以歸爲5類,包括3類數據讀問題(髒讀,不可得復讀和幻象讀)及兩類數據更新問題(第一類丟失更新和第二類丟失更新)。下面對每個併發問題進行說明。
-
髒讀(dirty read)
A事務讀取了B事務尚未提交的更改數據,並且在這個數據基礎上進行操作。如果此時恰巧B事務進行回滾,那麼A事務讀到的數據是根本不被承認的。以下是一個取款事務和轉賬事務併發時引起的髒讀場景。
時間 | 轉賬事務A | 取款事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢賬戶餘額爲1000元 | |
T4 | 取出500元,把餘額改爲500元 | |
T5 | 查詢賬戶餘額爲500元(髒讀) | |
T6 | 撤銷事務,餘額恢復爲1000元 | |
T7 | 匯入100元,餘額改爲600元 | |
T8 | 提交事務 |
在這個場景中,B希望取款500元,而後有撤銷了動作,而A往同一個賬戶轉賬100元,因爲A事務讀取了B事務尚未提交的數據,因而導致了賬戶白白丟失了500元。在Oracle數據中,不會發生髒讀的情況。
-
不可重複讀(unrepeatable read) 不可重複讀是指A事務讀取了B事務已經提交的更改數據。假設A在取款事務的過程中,B往該賬戶轉賬100元,A兩次讀取賬戶的餘額發生不一致
時間 | 取款事務A | 轉帳事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢賬戶餘額爲1000元 | |
T4 | 查詢賬戶餘額爲1000元 | |
T5 | 取出100元,把餘額改爲900元 | |
T6 | 提交事務 | |
T7 | 查詢賬戶餘額爲900元 |
在同一個事務中T4和T7時間點讀取的賬戶存款餘額不一致
-
幻象讀(phantom read)
A事務讀取B提交的新增數據,這時A事務將出現幻想讀的問題。幻讀一般發生在計算統計數據的事務中。舉個例子,假設銀行系統在同一個事務中兩次統計存款的總金額,在兩次統計過程中,剛好新增了一個存款賬戶,並存入100元,這時兩次統計的總金額將不一致。
時間 | 統計金額事務A | 轉帳事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 統計存款總金額爲10000元 | |
T4 | 新增一個存款賬戶,存款爲100元 | |
T5 | 提交事務 | |
T6 | 再次統計存款總金額爲10100元(幻象讀) |
如果新增的數據剛好滿足事務的查詢條件,那麼這個新數據就會進入事務的視野,因而導致兩次統計結果不一致的情況。 幻讀和不可重複讀是兩個容易混淆的概念,前者是指讀到了其他事物已經提交的新增數據,而後者是讀到了已經提交事務的更改數據(更改或刪除)。爲了避免這兩種情況,採取的策略是不同的:防止讀到更改數據,只需對操作的數據添加行級鎖,阻止操作過程中的數據發送變化,而防止讀到新增數據,則往往需要添加一個表級鎖–將整張表鎖定,防止新增數據(Oracle使用多版本數據的方式實現)
-
第一類丟失更新 A事務撤銷時,把已經提交的B事務的更新數據覆蓋了。這種錯可能會造成很嚴重的問題。通過下面的賬號取款轉賬就可以看出來。
時間 | 取款事務A | 轉賬事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢賬號餘額爲1000元 | |
T4 | 查詢賬號餘額爲1000元 | |
T5 | 匯入100元,把餘額改爲1100元 | |
T6 | 提交事務 | |
T7 | 取出100元,把餘額改爲900元 | |
T8 | 撤銷事務 | |
T9 | 餘額恢復爲1000元(丟失更新) |
A事務在撤銷時,“不小心”將B事務已經轉入賬號的金額給抹去了。
-
第二類丟失更新 A事務覆蓋B事務已經提交的數據,造成B事務所操作丟失。
時間 | 轉賬事務A | 取款事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢賬號餘額爲1000元 | |
T4 | 查詢餘額爲1000元 | |
T5 | 取出100元,把餘額改爲900元 | |
T6 | 提交事務 | |
T7 | 匯入100元,把餘額改爲1100元 | |
T8 | 提交事務 | |
T9 | 把餘額改爲1100元(丟失更新) |
在上面的例子,由於支票轉賬事務覆蓋了取款事務對存款餘額所做的更新,導致銀行最後損失了100元,相反如果轉賬事務先提交,那麼用戶損失了100元。
22.2數據庫鎖機制
數據併發會引發很多問題,在一些場合下有些問題是允許的,但在另外一些場合下可能卻是致命的。數據庫通過鎖的機制解決併發訪問的問題,雖然不同的數據庫在實現細節上存在差別,但原理基本上是一樣的。
按鎖定的對象的不同,一般可以分爲表鎖定和行鎖定,前者對整個表進行鎖定,而後者對錶中特定行進行鎖定。
從併發事務鎖定的關係上看,可以分爲共享鎖定和獨佔鎖定。共享鎖定會防止獨佔鎖定,但允許其它的共享鎖定。而獨佔鎖定既防止其它的獨佔鎖定,也防止其它的共享鎖定。爲了更改數據,數據庫必須在進行更改的行上施加行獨佔鎖定,INSERT、UPDATE、DELETE和SELECT FOR UPDATE語句都會隱式採用必要的行鎖定。
下面我們介紹一下數據庫常用的5種鎖定: 1.行共享鎖: SELECT FOR UPDATE 語句隱式獲取行共享鎖 2.行獨佔鎖: INSERT,UPDATE,DELETE 語句隱式獲取,或者通過LOCK TABLE IN ROW EXCLUSIVE MODE 獲取行獨佔鎖 3.表共享鎖: LOCK TABLE IN SHARE MODE 獲取,防止其他獨佔鎖獲取,但是允許在表內擁有多個行共享鎖和表共享鎖 4.表共享行獨佔鎖:LOCK TABLE IN SHARE ROW EXCLUSIVE MODE 獲取 5.表獨佔鎖 : LOCK TABLE IN EXCLUSIVE MODE
22.3事務隔離級別
儘管數據庫爲用戶提供了鎖的DML操作方式,但直接使用鎖管理是非常麻煩的,因此數據庫爲用戶提供了自動鎖機制。只要用戶指定會話的事務隔離級別,數據庫就會分析事務中的SQL語句,然後自動爲事務操作的數據資源添加上適合的鎖。此外數據庫還會維護這些鎖,當一個資源上的鎖數目太多時,自動進行鎖升級以提高系統的運行性能,而這一過程對用戶來說完全是透明的。 ANSI/ISO SQL 92標準定義了4個等級的事務隔離級別,在相同數據環境下,使用相同的輸入,執行相同的工作,根據不同的隔離級別,可以導致不同的結果。不同事務隔離級別能夠解決的數據併發問題的能力是不同的。 下表給出了 事務隔離級別對併發問題的解決情況
隔離級別 | 髒讀 | 不可重複讀 | 幻象讀 | 第一類丟失更新 | 第二類丟失更新 |
---|---|---|---|---|---|
READ UNCOMMITED | 允許 | 允許 | 允許 | 不允許 | 允許 |
READ COMMITTED | 不允許 | 允許 | 允許 | 不允許 | 允許 |
REPEATABLE READ | 不允許 | 不允許 | 允許 | 不允許 | 不允許 |
SERIALIZABLE | 不允許 | 不允許 | 不允許 | 不允許 | 不允許 |
事務的隔離級別和數據庫併發性是對立的,兩者此增彼長。一般來說,使用READ UNCOMMITED隔離級別的數據庫擁有最高的併發性和吞吐量,而使用SERIALIZABLE隔離級別的數據庫併發性最低。
SQL 92定義READ UNCOMMITED主要是爲了提供非阻塞讀的能力,Oracle雖然也支持READ UNCOMMITED,但它不支持髒讀,因爲Oracle使用多版本機制徹底解決了在非阻塞讀時讀到髒數據的問題並保證讀的一致性,所以,Oracle的READ COMMITTED隔離級別就已經滿足了SQL 92標準的REPEATABLE READ隔離級別。
SQL 92推薦使用REPEATABLE READ以保證數據的讀一致性,不過用戶可以根據應用的需要選擇適合的隔離等級。
測試案例:通過設置事務的隔離級別來解決不可重複讀的問題;
// @Transactional(isolation=Isolation.REPEATABLE_READ) @Transactional(isolation=Isolation.READ_COMMITTED) @Override public Integer getPartsSum(int partsid) { //獲取 Integer count=pr.getPartsNum(partsid); System.out.println("count:"+count); try { Thread.sleep(15000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //強制刷新緩存,或者在mapper文件中刷新;(mybatis默認使用了session級別的緩存) // pr.updateByPartsId(101, 35); //再次獲取數量 count=pr.getPartsNum(partsid); System.out.println("count:"+count); return count; }
@Transactional @Override public void partsOut(int partsId, int num) { Integer count=pr.getPartsNum(partsId); System.out.println("partsOut .count:"+count); System.out.println("開始修改"); pr.updateByPartsId(partsId, num); count=pr.getPartsNum(partsId); System.out.println("partsOut .count:"+count); System.out.println("完成修改"); }
22.4事務傳播特性
當一個事務方法被另外一個事務方法調用時,這時兩個方法中設置的事務的傳播屬性可以決定兩個事務如何組合(合成一個,還是分別處理);
即被調用的事務與調用的事務如何協同處理;
例如,A.a1中調用了B.b1方法;
這個兩個方的傳播屬性都是Required,那麼,它們將合併成一個事務;
Spring在TransactionDefinition接口中定義了7種類型的事務傳播行爲,它們規定了事務方法和事務方法發生嵌套調用時事務如何進行傳播;可以通過@Transactional(propagation=屬性值)來設置;
事務傳播行爲類型 | 說明 |
---|---|
PROPAGATION_REQUIRED | 如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇。 |
PROPAGATION_SUPPORTS | 支持當前事務,如果當前沒有事務,就以非事務方式執行。 |
PROPAGATION_MANDATORY | 使用當前的事務,如果當前沒有事務,就拋出異常。 |
PROPAGATION_REQUIRES_NEW | 新建事務,如果當前存在事務,把當前事務掛起。 |
PROPAGATION_NOT_SUPPORTED | 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。 |
PROPAGATION_NEVER | 以非事務方式執行,如果當前存在事務,則拋出異常。 |
PROPAGATION_NESTED | 如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作 |
注意:如果是PROPAGATION_REQUIRES_NEW,即使在外部的Service處理了異常,那麼外部事務和內部事務都是需要回退的。因爲內部回退了,所有外部也要回退;
NESTED說明:如果單獨運行則爲REQUIRED事務,如果放在另一個事務中,則爲NESTED設置一個保存點;這時,如果NESTED失敗,外部事務可以捕獲異常,並提交外部事務,如果外部事務失敗,NESTED 也將失敗;因此,它與REQUIRED_NEW和REQUIRED都是不同的;
當使用PROPAGATION_NESTED時,底層的數據源必須基於JDBC 3.0,並且實現者需要支持保存點事務機制。
//B事務方法, @Service public class PartsBillService { @Autowired PartsrepbillMapper prb; @Transactional(propagation=Propagation.NESTED) public void save(Partsrepbill bill) { prb.insert(bill); } }
@Service public class PartsServiceImpl implements PartsService { @Autowired BizPartsrepertoryMapper pr; @Autowired //注入B事務所在的Service PartsBillService pbs; //指定rollbackFor=Exception後,所有異常都回退 @Transactional(timeout=5,rollbackFor=Exception.class)//超時時間爲2秒 @Override public void partsOut(int partsId, int num) throws Exception { //開始計時 Integer count=pr.getPartsNum(partsId); System.out.println("partsOut .count:"+count); Partsrepbill bill=new Partsrepbill(); bill.setBillcount(5); bill.setBillid(11); bill.setBillflag("I"); bill.setBilltype("in1"); bill.setPartsid(100); bill.setBilltime(new Date()); bill.setBilluser(1); try { pbs.save(bill); }catch(Exception e) { System.out.println("訂單回退"); } System.out.println("開始修改"); pr.updateByPartsId(partsId, num); System.out.println("完成修改"); if(count>100) { //只爲測試使用; //回退 //throw new RuntimeException("ooo"); //不回退,提交 throw new Exception("ooo"); } System.out.println("修改完成;"+count); } }
22.5 設置回滾事務屬性;
rollbackFor:回滾異常類型
noRollbackFor:不回滾異常類型;
如果不設置兩個屬性;則Error及運行時異常回滾,受檢查異常不回滾;
rollbackFor和noRollbakcFor都可以配置,但最好配置成Exception,RuntimeException;如果這樣配置,那麼noRollbackFor會起作用
//指定rollbackFor=Exception後,所有異常都回退 @Transactional(timeout=2,rollbackFor=Exception.class)//超時時間爲2秒 @Override public void partsOut(int partsId, int num) throws Exception { //開始計時 Integer count=pr.getPartsNum(partsId); System.out.println("partsOut .count:"+count); System.out.println("開始修改"); pr.updateByPartsId(partsId, num); System.out.println("完成修改"); if(count>10) { //回退 //throw new RuntimeException("ooo"); //不回退,提交 throw new Exception("ooo"); } pr.updateByPartsId(partsId, num); System.out.println("修改完成;"+count); }
22.6 timeout超時
所謂事務超時,就是指一個事務所允許執行的最長時間,如果超過該時間限制但事務還沒有完成,則自動回滾事務。在 TransactionDefinition 中以 int 的值來表示超時時間,其單位是秒。 默認設置爲底層事務系統的超時值,如果底層數據庫事務系統沒有設置超時值,那麼就是none,沒有超時限制。
Spring事務超時 = 事務開始時到最後一個Statement創建時時間 + 最後一個Statement的執行完的時間(即其queryTimeout)。所以在在執行Statement之外的超時無法進行事務回滾
如果要讓此屬性生效,需要定義兩個操作方法(例如 update),並在操作之間使用Thread.sleep(來進行測試)
@Transactional(timeout=2)//超時時間爲2秒 @Override public void partsOut(int partsId, int num) { //開始計時 Integer count=pr.getPartsNum(partsId); System.out.println("partsOut .count:"+count); //休眠3秒,此時拋出異常 try { Thread.sleep(3000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("開始修改"); pr.updateByPartsId(partsId, num); System.out.println("完成修改"); }
22.7 readOnly
只讀事務,如果沒有修改的操作,會在運行時做一定的優化;