再談數(shù)據(jù)庫的事務隔離性
寫在前面
近兩年分布式數(shù)據(jù)庫技術加速發(fā)展,而由于金融行業(yè)技術生態(tài)的限制,周圍很多同學對其并沒有深入的了解,所以進行高性能、高可靠系統(tǒng)設計時往往缺少這一利器。Ivan希望以系列文章的方式與大家交流探討,加深我們對分布式數(shù)據(jù)庫的認識。
本文是該系列文章的***篇,主要探討事務管理中的隔離性,厘清相關概念和關鍵技術,為后面闡述分布式數(shù)據(jù)庫的事務管理做一個鋪墊,姑且算是一篇前傳吧。
正文
我們首先從定義出發(fā),事務管理包括原子性、一致性、隔離性和持久性四個方面,即ACID。所有數(shù)據(jù)庫專著都會給出這個四個特性的定義,本文我們引用了Jim Gray對其的定義。
Jim Gray是事務處理方面的大師,本文中很多內(nèi)容都來自他的專著和論文。為避免翻譯引入的歧義,這里我們直接引用原文。
Atomicity: Either all the changes from the transaction occur (writes, and messages sent), or none occur.
Consistency: The transaction preserves the integrity of stored information.
Isolation: Concurrently executing transactions see the stored information as if they were running serially (one after another).
Durability: Once a transaction commits, the changes it made (writes and messages sent) survive any system failures.
在上述隔離性(Isolation)的定義中,我們可以發(fā)現(xiàn)其目標是使并發(fā)事務的執(zhí)行效果與串行一致,但在具體技術實現(xiàn)上往往需要在并發(fā)能力和串行化效果之間進行平衡,很難兩者兼顧。
平衡的結果就是會出現(xiàn)違反串行效果的現(xiàn)象即異常現(xiàn)象(Phenomenon)。通常來說,隔離級別的提升伴隨著并發(fā)能力的下降,兩者負相關。各種數(shù)據(jù)庫在談到隔離級別時都會引用ANSI SQL-92標準隔離級別,我們來看看它的具體內(nèi)容。
ANSI SQL-92 Isolation Levels
ANSI SQL-92可能是最早提出了基于異常現(xiàn)象來定義隔離級別的方法,同時沒有將隔離級別與具體實現(xiàn)機制綁定,隔離的實現(xiàn)可以基于鎖(lock-based)或者無鎖(lock-free),兼容了后續(xù)的技術發(fā)展。該標準根據(jù)三種異常現(xiàn)象將隔離性定義為四個級別,具體如下。
臟讀,事務(T1)中修改的數(shù)據(jù)項在尚未提交的情況下被其他事務(T2)讀取到,而T1進行Rollback操作,則T2剛剛讀取到的數(shù)據(jù)并沒有實際存在。
不可重復讀,T1讀取數(shù)據(jù)項,T2對其中的數(shù)據(jù)進行了修改或刪除且Commit成功。如果T1嘗試再次讀取這些數(shù)據(jù),會得到T2修改后的數(shù)據(jù)或者發(fā)現(xiàn)數(shù)據(jù)已刪除。這樣T1在一個事務中兩次同樣條件的讀取,且結果集內(nèi)容變更或結果集數(shù)量減少。
幻讀,T1使用特定的查詢條件獲得一個結果集,T2插入新的數(shù)據(jù)且這些數(shù)據(jù)符合T2剛剛操作的查詢條件。T2 commit 成功后,T1再次執(zhí)行同樣的查詢,此時得到的結果集增大。
很多文章都結合數(shù)據(jù)庫產(chǎn)品對上述異?,F(xiàn)象的實例和處理機制進行了說明,本文中不再贅述,有興趣的同學可以參考文末的鏈接[1]。
ANSI SQL-92標準早在1992年發(fā)布,但無論是當時還是后來都沒有被各大數(shù)據(jù)庫廠商嚴格遵循,部分原因可能是標準過于簡化與實際應用有一定程度的脫離。Jim Gray等人在1995發(fā)布了論文“A Critique of ANSI SQL Isolation Levels” (本文中簡稱為Critique[2]),對隔離級別進行更全面的闡述,可以幫助我們加深理解。
Critique Isolation Levels
Critique提出了ANSI SQL-92存在的兩個問題,首先是自然語言方式界定的異?,F(xiàn)象并不嚴格導致一些同質(zhì)化的異常現(xiàn)象被遺漏;其次是一些典型的異?,F(xiàn)象并沒有被涵蓋進去,導致隔離級別存在明顯缺失。
因此,文中對ANSI SQL-92的三種異常現(xiàn)象(將其編號為A1/A2/A3)進行了擴展(編號為P1/P2/P3),并增加了另外5種常見的異常現(xiàn)象。受限于篇幅,這里僅對兩種異?,F(xiàn)象進行說明。
Lost Update
丟失更新(Lost Update)是一個經(jīng)典的數(shù)據(jù)庫問題,由于太過重要所有主流數(shù)據(jù)庫都解決了該問題,我們這里將操作稍加變形來舉例。
我們使用MySQL進行演示,創(chuàng)建表并初始化數(shù)據(jù):
- create table account (balance int,name varchar(20)) ENGINE=InnoDB;
- insert into account values(50,'Tom');
T1 |
T2 |
begin; |
begin; |
select balance into @bal from account where name='Tom' |
|
|
select balance into @bal from account where name='Tom' |
|
update account set balance = @bal -40 where name = ‘Tom’; |
|
commit; |
update account set balance = @bal - 1 where name = ‘Tom’; |
|
commit; |
|
在上述操作中T1、T2串行執(zhí)行效果是對余額進行兩次扣減,分別為40和1,最終值為9,但并行的最終值為49,T2的修改被丟失。我們可以發(fā)現(xiàn)Lost update的實質(zhì)是T1事務讀取數(shù)據(jù),而后該數(shù)據(jù)被T2事務修改并提交,T1基于已經(jīng)過期的數(shù)據(jù)進行了再次修改,造成T2的修改被覆蓋。
Read Skew
讀偏序(Read Skew)是RC級遇到的問題。如果數(shù)據(jù)項x與y存在一致性約束,T1先對讀x,而后T2修改x和y后commit,此時T1再讀y。T1得到的x與y不滿足原有的一致性約束。
MySQL默認隔離級別為RR,我們需要手工設置為RC并初始化數(shù)據(jù):
- set session transaction isolation level read committed;
- insert into account values(70,'Tom');
- insert into account values(30,'Kevin');
T1 |
T2 |
begin; |
begin; |
select * from account where name=’Tom’; |
|
|
select * from account where name=’Tom’; |
|
update account set balance = balance - 30 where name='Tom'; |
|
update account set balance = balance + 30 where name=’Kevin’; |
|
commit; |
select * from account where name='Kevin'; |
|
commit; |
|
初始數(shù)據(jù)Tom與Kevin的賬戶合計為100,在T1事務內(nèi)的兩次讀取得到賬戶合計為130,顯然不符合之前的一致性約束。
補充這些異?,F(xiàn)象后,Critique給出了新的矩陣,相比ANSI更加完善也更貼合真實的數(shù)據(jù)庫產(chǎn)品。
主流數(shù)據(jù)庫考慮到串行化效果與并發(fā)性能的平衡,一般默認隔離級別介于RC與RR之間,部分提供了Serializable。特別提醒,無論ASNI SQL-92還是Critique的隔離級別都不能確保直接映射到實際數(shù)據(jù)庫的同名隔離級別。
SI&MVCC
快照隔離(SI,Snapshot Isolation)是討論隔離性時常見的術語,可以做兩種的解讀,一是具體的隔離級別,SQL Server、CockroachDB都直接定義了這個隔離級別;二是一種隔離機制用于實現(xiàn)相應的隔離級別,在Oracle、MySQL InnoDB、PostgreSQL等主流數(shù)據(jù)庫中普遍使用。
多版本并發(fā)控制(MVCC,multiversion concurrency control)是通過記錄數(shù)據(jù)項歷史版本的方式提升系統(tǒng)應對多事務訪問的并發(fā)處理能力,例如避免單值(Single-Valued)存儲情況下寫操作對讀操作的鎖排斥。MVCC和鎖都是SI的重要實現(xiàn)手段,當然也存在無鎖的SI實現(xiàn)。以下是Critique描述的SI運作過程。
事務(記為T1)開始的瞬間會獲取一個時間戳Start Timestamp(記為ST),而數(shù)據(jù)庫內(nèi)的所有數(shù)據(jù)項的每個歷史版本都記錄著對應的時間戳Commit Timestamp(記為CT)。T1讀取的快照由所有數(shù)據(jù)項版本中那些CT小于ST且最近的歷史版本構成,由于這些數(shù)據(jù)項內(nèi)容只是歷史版本不會再次被寫操作鎖定,所以不會發(fā)生讀寫沖突,快照內(nèi)的讀操作永遠不會被阻塞。其他事務在ST之后的修改,T1不可見。當T1 commit的瞬間會獲得一個CT,并保證大于此刻數(shù)據(jù)庫中已存在的任意時間戳(ST或CT),持久化時會將這個CT作為數(shù)據(jù)項的版本時間戳。T1的寫操作也體現(xiàn)在T1的快照中,可以被T1內(nèi)的讀操作再次讀取。當T1 commit后,修改會對那些持有ST大于T1 CT的事務可見。
如果存在其他事務(T2),其CT在T1的運行間隔【ST,CT】之間,與T1對同樣的數(shù)據(jù)項進行寫操作,則T1 abort,T2 commit成功,這個特性被稱為First-committer-wins,可以保證不出現(xiàn)Lost update。事實上,部分數(shù)據(jù)庫會將其調(diào)整為First-write-wins,將沖突判斷提前到write操作時,減少沖突的代價。
這個過程不是某個數(shù)據(jù)庫的具體實現(xiàn),事實上不同數(shù)據(jù)庫對于SI實現(xiàn)存在很大差別。例如,PostgreSQL會將歷史版本和當前版本一起保存通過時間戳區(qū)分,而MySQL和Oracle都在回滾段中保存歷史版本。MySQL的RC與RR級別均使用了SI,如果當前事務(T1)讀操作的數(shù)據(jù)被其他事務的寫操作加鎖,T1轉(zhuǎn)向回滾段讀取快照數(shù)據(jù),避免讀操作被阻塞。但是RC的快照定義與以上描述不同,也包括了T1執(zhí)行過程中其他事務提交的***版本[6]。
此外,我們還有一個重要發(fā)現(xiàn),時間戳是生成SI的關鍵要素。在單機系統(tǒng)中,唯一時間戳比較容易實現(xiàn),而對于分布式系統(tǒng)在跨節(jié)點、跨數(shù)據(jù)中心甚至跨城市部署的情況下如何建立一個唯一時鐘就成為一個非常復雜的問題,我們暫留下一個伏筆將在后面的專題文章中進行討論。
Serializable VS SSI
SI是如此有效,甚至在TPC-C benchmark測試中也沒有出現(xiàn)任何異常現(xiàn)象[5],但事實上SI不能保證完整的串行化效果。Critique中指出,SI還無法處理A5B(Write Skew,寫偏序),如下圖所示:
Write Skew
寫偏序(Write Skew)也是一致性約束下的異常現(xiàn)象,即兩個并行事務都基于自己讀到的數(shù)據(jù)集去覆蓋另一部分數(shù)據(jù)集,在串行化情況下兩個事務無論何種先后順序,最終將達到一致狀態(tài),但SI隔離級別下無法實現(xiàn)。下圖的“黑白球”常常被用來說明寫偏序問題。
如何實現(xiàn)真正的串行化效果呢?事實上,早期的數(shù)據(jù)庫已經(jīng)通過嚴格兩階段鎖協(xié)議(S2PL,Strict Two-Phase Locking)實現(xiàn)了完全的串行化隔離(Serializable Isolation),即正在進行讀操作的數(shù)據(jù)阻塞對應寫操作,寫操作阻塞所有操作(包括讀操作和寫操作)。如阻塞造成循環(huán)將構成死鎖,則需要進行rollback操作。S2PL的問題顯而易見,在競爭激烈場景下,阻塞和死鎖會造成數(shù)據(jù)庫吞吐量下降和響應時間的增加,所以這種串行化無法應用于實際生產(chǎn)環(huán)境。直到SSI的出現(xiàn),人們終于找到具有實際價值的串行化隔離方案。
串行化快照隔離(SSI, Serializable Snapshot Isolation,也被翻譯為序列化快照)是基于SI改進達到Serializable級別的隔離性。SSI由Michael James Cahill在他的論文"Serializable Isolation for Snapshot Databases"[3]中提出(該論文獲得2008 Sigmod Best Paper Award,文章末尾提供了該論文的2009年完整版[4]相關信息,有興趣的同學可以深入研究)。
SSI保留了SI的很多優(yōu)點,特別是讀不阻塞任何操作,寫不會阻塞讀。事務依然在快照中運行,但增加了對事務間讀寫沖突的監(jiān)控用于識別事務圖(transaction graph)中的危險結構。當一組并發(fā)事務可能產(chǎn)生異?,F(xiàn)象(anomaly),系統(tǒng)將通過回滾其中某些事務進行干預以消除anomaly發(fā)生的可能。這個過程雖然會導致某些事務的錯誤回滾(不會導致anomaly的事務被誤殺),但可以確保消除anomaly[3]。
從理論模型看,SSI性能接近SI,遠遠好于S2PL。2012年,PostgreSQL在9.1版本中實現(xiàn)了SSI[7],可能也是***支持SSI的商業(yè)數(shù)據(jù)庫,驗證了SSI的實現(xiàn)效果。CockroachDB也從Cahill的論文獲得靈感,實現(xiàn)SSI并將其作為其默認隔離級別。
隨著技術的發(fā)展,SI/SSI已經(jīng)成為主流數(shù)據(jù)庫的隔離技術,尤其是后者的出現(xiàn),無需開發(fā)人員在代碼通過顯式鎖來避免異常,從而降低了人為錯誤的概率。在分布式數(shù)據(jù)庫的相關章節(jié)中,我們將進一步對SSI實現(xiàn)機制進行深入探討。
作者:
王磊(Ivan),現(xiàn)任職光大銀行領域架構師,曾任職于IBM全球咨詢服務部從事技術咨詢工作,具有十余年數(shù)據(jù)領域研發(fā)及咨詢經(jīng)驗。目前負責全行數(shù)據(jù)領域系統(tǒng)的日常架構管理、重點系統(tǒng)架構設計及內(nèi)部研發(fā)等工作。個人公眾號:金融數(shù)士。