本地事務
事務特性:ACID,其中C一致性是目的,AID是手段。
實現隔離性
寫鎖:數據加了寫鎖,其他事務不能寫也不能讀。
讀鎖:數據加了讀鎖,其他事務不能加寫鎖可以加讀鎖,可以允許自己升級為寫鎖。
范圍鎖:對某個范圍加寫鎖,范圍內數據不能寫入。
隔離級別
以鎖為手段來實現隔離性才是數據庫表現出不同隔離級別的根本原因。
可串行化:對事務所有讀、寫數據加上三種鎖。
可重復讀:不加范圍鎖,會有幻讀問題。
幻讀是指在事務執行過程中,兩個完全相同的范圍查詢得到了不同的結果集。譬如現在准備統計一下 Fenix's Bookstore 中售價小於 100 元的書有多少本,會執行以下第一條 SQL 語句:
SELECT count(1) FROM books WHERE price < 100 /* 時間順序:1,事務: T1 */
INSERT INTO books(name,price) VALUES ('深入理解Java虛擬機',90) /* 時間順序:2,事務: T2 */
SELECT count(1) FROM books WHERE price < 100 /* 時間順序:3,事務: T1 */
兩次執行之間有另外一個事務在數據庫插入了一本小於 100 元的書籍,那這兩次相同的查詢就會得到不
一樣的結果,原因是可重復讀沒有范圍鎖來禁止在該范圍內插入新的數據,這是一個事務受到其他事務影響,隔離性被破壞的表現。
讀已提交:寫鎖會一直持續到事務結束,讀鎖在查詢操作完成后馬上釋放。有不可重復讀問題,讀已提交的隔離級別缺乏貫穿整個事務周期的讀鎖,無法禁止讀取過的數據發生變化。
讀未提交:有臟讀問題。
MVCC
MVCC是並發訪問控制技術,解決了讀寫沖突問題(幻讀)。
基本思路是對數據庫的修改不會直接覆蓋之前的數據,而是產生一個新版副本與老版本共存。
“版本”可以理解為每一行記錄存在倆看不見的字段:CREATE_VERRSION和DELETE_VERSION,這兩個字段都是事務ID,事務ID是全局遞增的值,根據以下規則寫入數據。
- 插入數據:CREATE_VERRSION記錄插入數據的事務ID,DELETE_VERSION為空。
- 刪除數據:DELETE_VERSION記錄刪除數據的事務ID,CREATE_VERRSION為空。
- 修改數據:將修改數據視為“刪除舊數據,插入新數據”,將原有數據復制一份,原有數據DELETE_VERSION記錄修改數據的事務ID。復制出來的新數據CREATE_VERSION記錄修改數據的事務ID。
此時,有另一個事務讀取這些發生了變化的數據,根據隔離級別決定讀取哪個版本的數據。
- 可重復讀:在“總是讀取CREATE_VERSION小於等於當前事務ID的數據”前提下,如果數據有多個版本,讀取事務ID最大的。
- 讀已提交:總是讀取最新版本,即最近被Commit版本的數據。
MVCC是針對“讀+寫”的優化,“寫+寫”只能加鎖解決。競爭激烈的情況下,樂觀鎖可能更慢。
MVCC超售問題
數據庫采用的是MVCC方案,是否有可能出現以下這種超售情況
初始quantity值為10,事務T1和事務T2都想要將quantity減8
SELECT quantity FROM books WHERE id=1 /* 時間順序:1,事務: T1 */
SELECT quantity FROM books WHERE id=1 /* 時間順序:2,事務: T2 */
/*事務: T1 運算后將quantity改為2 因為MVCC方案中事務2的select並不會加讀鎖,所以這條語句可以順利執行並commit*/
UPDATE books SET quantity=2 WHERE id=1 /* 時間順序:3,事務: T1 */
commit
/*事務: T2 運算后將quantity改為2 ,此時沒有其他的事務了,所以這條語句可以順利執行並commit*/
UPDATE books SET quantity=2 WHERE id=1 /* 時間順序:4,事務: T2 */
commit
這種寫法會出現超售,相當於賣了兩次8本書。
之前提到過,MVCC只解決“讀-寫”事務的情況(也就是解決可重復讀級別下的幻讀),在“寫-寫”的場景中它是不適用的。
也正是為了解決這類情況,InnoDB之類采用MVCC的引擎,都會提供諸如“lock in share mode”的語法,讓開發者在“寫-寫”的場景中顯式加共享鎖,讓數據庫進行當前讀而非快照讀。
以MySQL為例,把代碼修改為這樣,它就可以保證T1的Update語句被T2的共享鎖阻塞了,達到避免超售的目的了。
SELECT quantity FROM books WHERE id=1 lock in share mode; /* 時間順序:1,事務: T1 */
SELECT quantity FROM books WHERE id=1 lock in share mode; /* 時間順序:2,事務: T2 */
全局事務
2PC
假如你平時以聲明式事務來編碼,那它與本地事務看起來可能沒什么區別,都是標個@Transactional注解而已,但如果以編程式事務來實現的話,就能在寫法上看出差異,偽代碼如下所示:
public void buyBook(PaymentBill bill) {
userTransaction.begin();
warehouseTransaction.begin();
businessTransaction.begin();
try {
userAccountService.pay(bill.getMoney());
warehouseService.deliver(bill.getItems());
businessAccountService.receipt(bill.getMoney());
userTransaction.commit();
warehouseTransaction.commit();
businessTransaction.commit();
} catch(Exception e) {
userTransaction.rollback();
warehouseTransaction.rollback();
businessTransaction.rollback();
}
}
從代碼上可看出,程序的目的是要做三次事務提交,但實際上代碼並不能這樣寫,試想一下,如果在
businessTransaction.commit()中出現錯誤,代碼轉到catch塊中執行,此時userTransaction和
warehouseTransaction已經完成提交,再去調用rollback()方法已經無濟於事,這將導致一部分數據被提
交,另一部分被回滾,整個事務的一致性也就無法保證了。
為了解決這個問題,XA 將事務提交拆分成為兩階段過程:
- 准備階段:又作投票階段,協調者詢問事務的所有參與者是否准備好提交,參與者如果准備好提交則回復Prepared。對於數據庫來說,准備操作是在重做日志中記錄全部事務提交操作所要做的內容,不釋放隔離性,繼續持有鎖。
- 提交階段:又作執行階段,協調者在上一階段收到Prepared消息,先自己在本地持久化事務狀態為Commit,操作完成之后向所有參與者發送Commit指令;否則,任意一個參與者回復了 Non-Prepared 消息,或任意一個參與者超時未回復,協調者將將自己的事務狀態持久化為 Abort 之后,向所有參與者發送 Abort 指令,參與者立即執行回滾操作。
缺點
-
單點問題:協調者在兩段提交中具有舉足輕重的作用,協調者等待參與者回復時可以有超時機制,允許參與者宕機,但參與者等待協調者指令時無法做超時處理。一旦宕機的不是其中某個參與者,而是協調者的話,所有參與者都會受到影響。如果協調者一直沒有恢復,沒有正常發送 Commit 或者 Rollback 的指令,那所有參與者都必須一直等待。
-
性能問題:兩段提交過程中,所有參與者相當於被綁定成為一個統一調度的整體,期間要經過兩次遠程服務調用,三次數據持久化(准備階段寫重做日志,協調者做狀態持久化,提交階段在日志寫入 Commit Record),整個過程將持續到參與者集群中最慢的那一個處理操作結束為止,這決定了兩段式提交的性能通常都較差。
-
一致性風險
3PC
三段式提交把原本的兩段式提交的准備階段細分為兩個階段,分別稱為CanCommit、PreCommit,提交階段改為
DoCommit階段。CanCommit是詢問階段,協調者讓每個參與者根據自身狀態評估事務是否可能完成。
將准備階段一分為二的理由是:協調者發出開始准備的消息,參與者開始寫重做日志,如果此時,某一個參與者宣布無法完成,相當於大家做了一輪無用功。
因此,在事務需要回滾的場景中,三段式的性能通常是要比兩段式好很多的,但在事務能夠正常提交的場景中,兩者的性能都依然很差,甚至三段式因為多了一次詢問,還要稍微更差一些。
同樣也是由於事務失敗回滾概率變小的原因,在三段式提交中,如果在 PreCommit 階段之后發生了協調者宕機,即參與者沒有能等到 DoCommit 的消息的話,默認的操作策略將是提交事務而不是回滾事務或者持續等待,這就相當於避免了協調者單點問題的風險。
分布式事務
CAP與ACID
柔性事務與最終一致性。
可靠事件隊列
最大努力一次提交:將最有可能出錯的業務以本地事務的方式完成后,采用不斷重試的方式促使分布式事務中的其他關聯業務全部完成。
TCC事務
如果業務需要隔離,該方案天生適合用於需要強隔離性的分布式事務中。
TCC 較為煩瑣,它是一種業務侵入式較強的事務方案,要求業務處理過程必須拆分為“預留業務資源”和“確認/釋放消費資源”兩個子過程。如同 TCC 的名字所示,它分為以下三個階段。
- Try:嘗試執行階段,完成所有業務可執行性的檢查(保障一致性),並且預留好全部需用到的業務資源(保障隔離性)。
- Confirm:確認執行階段,不進行任何業務檢查,直接使用 Try 階段准備的資源來完成業務處理。Confirm 階段可能會重復執行,因此本階段所執行的操作需要具備冪等性。
- Cancel:取消執行階段,釋放 Try 階段預留的業務資源。Cancel 階段可能會重復執行,也需要滿足冪等性。
Reference
《鳳凰架構》