本文節選自: 關係型數據庫理論 https://url.wx-coder.cn/DJNQn ,涉及引用/整理的文章列舉在了 Database-List。
數據庫系統的萌芽出現於 60 年代。當時計算機開始普遍地應用於數據管理,對數據的共享提出了愈來愈高的要求。傳統的文件系統已經不能知足人們的須要。可以統一管理和共享數據的數據庫管理系統(DBMS)應運而生。1961 年通用電氣公司(General ElectricCo.)的 Charles Bachman 成功地開發出世界上第一個網狀 DBMS 也是第一個數據庫管理系統—— 集成數據存儲(Integrated DataStore IDS),奠基了網狀數據庫的基礎。git
1970 年,IBM 的研究員 E.F.Codd 博士在刊物 Communication of the ACM 上發表了一篇名爲「A Relational Modelof Data for Large Shared Data Banks」的論文,提出了關係模型的概念,奠基了關係模型的理論基礎。1974 年,IBM 的 Ray Boyce 和 DonChamberlin 將 Codd 關係數據庫的 12 條準則的數學定義以簡單的關鍵字語法表現出來,里程碑式地提出了 SQL(Structured Query Language)語言。在很長的時間內,關係數據庫(如 MySQL 和 Oracle)對於開發任何類型的應用程序都是首選,巨石型架構也是應用程序開發的標準架構。github
本文便是對關係型數據庫中的事務管理相關內容進行討論。數據庫
事務提供一種全作,或不作(All or Nothing)的機制,即將一個活動涉及的全部操做歸入到一個不可分割的執行單元,組成事務的全部操做只有在全部操做均能正常執行的狀況下方能提交,只要其中任一操做執行失敗,都將致使整個事務的回滾。數據庫事務具備 ACID 屬性,即原子性(Atomic)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability),在分佈式事務 https://url.wx-coder.cn/7p8Xx 中咱們也會討論分佈式系統中應該如何實現事務機制。編程
ACID 包含了描述事務操做的總體性的原子性,描述事務操做下數據的正確性的一致性,描述事務併發操做下數據的正確性的隔離性,描述事務對數據修改的可靠性的持久性。針對數據庫的一系列操做提供了一種從失敗狀態恢復到正常狀態的方法,使數據庫在異常狀態下也可以保持數據的一致性,且面對併發訪問時,數據庫可以提供一種隔離方法,避免彼此間的操做互相干擾。網絡
SQL 標準定義了 4 類隔離級別,包括了一些具體規則,用來限定事務內外的哪些改變是可見的,哪些是不可見的。低級別的隔離級通常支持更高的併發處理,並擁有更低的系統開銷。數據結構
隔離級別 | 髒讀(Dirty Read ) | 不可重複讀(NonRepeatable Read ) | 幻讀(Phantom Read ) |
---|---|---|---|
未提交讀(Read Uncommitted) | 可能 | 可能 | 可能 |
提交讀(Read Committed ) | 不可能 | 可能 | 可能 |
可重複讀(Repeatable Read ) | 不可能 | 不可能 | 可能 |
可串行化(Serializable ) | 不可能 | 不可能 | 不可能 |
在該隔離級別,全部事務均可以看到其餘未提交事務的執行結果。本隔離級別不多用於實際應用,由於它的性能也不比其餘級別好多少。讀取未提交的數據,也被稱之爲髒讀(Dirty Read)。架構
這是大多數數據庫系統的默認隔離級別好比 Sql Server, Oracle 等,但不是 MySQL 默認的。它知足了隔離的簡單定義:一個事務只能看見已經提交事務所作的改變。這種隔離級別也支持所謂的不可重複讀(Nonrepeatable Read),由於同一事務的其餘實例在該實例處理其間可能會有新的 Commit,因此同一查詢可能返回不一樣結果。併發
當隔離級別設置爲 Repeatable Read 時,能夠避免不可重複讀。不可重複讀是指事務 T1 讀取數據後,事務 T2 執行更新操做,使 T1 沒法再現前一次讀取結果。具體地講,不可重複讀包括三種狀況:分佈式
這是 MySQL 的默認事務隔離級別,它確保在一個事務內的相同查詢條件的屢次查詢會看到一樣的數據行,都是事務開始時的數據快照。雖然 Repeatable Read 避免了不可重複讀,但還有可能出現幻讀。簡單說,就是當某個事務在讀取某個範圍內的記錄時,另外的一個事務又在該範圍內插入新的記錄。在以前的事務在讀取該範圍的記錄時,就會產生幻行,InnoDB 經過間隙鎖(next-key locking)策略防止幻讀的出現。工具
Serializable 是最高的事務隔離級別,它經過強制事務排序,使之不可能相互衝突,從而解決幻讀問題。簡言之,它是在每一個讀的數據行上加上共享鎖。在這個級別,可能致使大量的超時現象和鎖競爭。該隔離級別代價也花費最高,性能很低,通常不多使用,在該級別下,事務順序執行,不只能夠避免髒讀、不可重複讀,還避免了幻讀。
併發控制旨在針對數據庫中對事務並行的場景,保證 ACID 中的一致性(Consistency)與隔離性(Isolation)。假如全部的事務都僅進行數據讀取,那麼事務之間並不會有衝突;而一旦某個事務讀取了正在被其餘事務修改的數據或者兩個事務修改了相同的數據,那麼數據庫就必須來保證事務之間的隔離,來避免某個事務由於未見最新的數據而形成的誤操做。解決併發控制問題最理想的方式就是可以每當某個事務被建立或者中止的時候,監控全部事務的全部操做,判斷是否存在衝突的事務,而後對衝突事務中的操做進行重排序以儘量少地減小衝突,然後以特定的順序運行這些操做。絕大部分數據庫會採用鎖(Locks)或者數據版本控制(Data Versioning)的方式來處理併發控制問題。
數據庫技術中主流的三種併發控制技術分別是: Multi-version Concurrency Control (MVCC), Strict Two-Phase Locking (S2PL), 以及 Optimistic Concurrency Control (OCC),每種技術也都有不少的變種。在 MVCC 中,每次寫操做都會在舊的版本之上建立新的版本,而且會保留舊的版本。當某個事務須要讀取數據時,數據庫系統會從全部的版本中選取出符合該事務隔離級別要求的版本。MVCC 的最大優點在於讀並不會阻塞寫,寫也不會阻塞讀;而像 S2PL 這樣的系統,寫事務會事先獲取到排他鎖,從而會阻塞讀事務。PostgreSQL 以及 Oracle 等 RDBMS 實際使用了所謂的 Snapshot Isolation(SI)這個 MVCC 技術的變種。Oracle 引入了額外的 Rollback Segments,當寫入新的數據時,老版本的數據會被寫入到 Rollback Segment 中,隨後再被覆寫到實際的數據塊。PostgreSQL 則是使用了相對簡單的實現方式,新的數據對象會被直接插入到關聯的 Table Page 中;而在讀取表數據的時候,PostgreSQL 會經過可見性檢測規則(Visibility Check Rules)來選擇合適的版本。
基於鎖的方式基礎理念爲:若是某個事務須要數據,則對數據加鎖,操做完畢後釋放鎖;若是過程當中其餘事務須要鎖,則須要等到該事務釋放數據鎖,這種鎖也就是所謂的排他鎖(Exclusive Lock)。不過使用排他鎖會帶來極大的性能損耗,其會致使其餘那些僅須要讀取數據的事務也陷入等待。另外一種加鎖的方式稱爲共享鎖(Shared Lock),當兩個事務都聲明讀取數據 A 時,它們會分別給 A 添加共享鎖;對於此事須要修改數據 A 的事務而言,它必須等待全部的共享鎖釋放完畢以後才能針對數據 A 添加排他鎖。一樣地,對於已經被設置了排他鎖的數據,僅有讀取請求的事務一樣須要等到該排他鎖被釋放後才能添加共享鎖。
從鎖定的數據範圍鎖粒度(Lock Granularity)來看分爲:
鎖管理器(Lock Manager)即負責分配與釋放鎖,大部分數據庫是以哈希表的方式來存放持有鎖以及等待鎖的事務。在 MySQL 實戰 https://url.wx-coder.cn/Tu5dq 中咱們也討論瞭如何觸發鎖機制,譬如查詢加鎖,select * from testlock where id=1 for update;
,即查詢時不容許更改,該語句在自動提交爲 off 或事務中生效,至關於更改操做,模擬加鎖;而更像類操做 update testlock name=name;
則是會自動加鎖。
一樣的,參考併發編程導論 https://url.wx-coder.cn/Yagu8 中的討論,只要存在鎖的地方就會存在死鎖(Deadlock)的可能性:
在發生死鎖的時候,鎖管理器會根據必定的規則來選取應該終止或者被回滾的事務:
避免死鎖,確保純隔離的最簡單方法是在事務開始時獲取鎖並在事務結束時釋放鎖。這意味着事務必須在啓動以前等待其全部鎖,而且在事務結束時釋放事務持有的鎖,這種方式會浪費不少時間來等待全部鎖。實際的數據庫,譬如 DB2 與 SQL Server 中每每採起兩階段鎖協議(Two-Phase Locking Protocol),即將事務過程切分爲兩個階段:
該策略可以減小其餘事務等待鎖的時間,而且避免某個事務在中途修改了並非它初次申請的數據。
在併發編程導論 https://url.wx-coder.cn/Yagu8 中咱們討論了兩種不一樣類型的鎖:樂觀鎖(Optimistic Lock)與悲觀鎖(Pessimistic Lock),前文介紹的各類鎖便是悲觀鎖,而 MVCC(Multiple Version Concurrency Control) 這樣的基於數據版本的鎖則是樂觀鎖,它可以保證讀寫操做之間不會相互阻塞:
樂觀鎖,大可能是基於數據版本(Version)記錄機制實現。數據版本即爲數據增長一個版本標識,在基於數據庫表的版本解決方案中,通常是經過爲數據庫表增長一個 version
字段來實現。讀取出數據時,將此版本號一同讀出,以後更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,若是提交的數據版本號大於數據庫表當前版本號,則予以更新,不然認爲是過時數據。而 PostgreSQL 中則是依賴於 txid 以及 Commit Log 結合而成的可見性檢測機制來實現 MVCC,詳情能夠參考 PostgreSQL 架構機制 https://url.wx-coder.cn/SgRDQ 中關於併發控制相關的介紹。
數據庫事務由具體的 DBMS 系統來保障操做的原子性,同一個事務當中,若是有某個操做執行失敗,則事務當中的全部操做都須要進行回滾,回到事務執行前的狀態。致使事務失敗的緣由有不少,多是由於修改不符合表的約束規則,也有多是網絡異常,甚至是存儲介質故障等,而一旦事務失敗,則須要對全部已做出的修改操做進行還原,使數據庫的狀態恢復到事務執行前的狀態,以保障數據的一致性,使修改操做要麼所有成功、要麼所有失敗,避免存在中間狀態。
訪問磁盤中的數據每每速度較慢,換言之,內存中數據的訪問速度仍是遠快於 SSD 中的數據訪問速度。基於這個考量,基本上全部數據庫引擎都儘量地避免訪問磁盤數據。而且不管數據庫表仍是數據庫索引都被劃分爲了固定大小的數據頁(譬如 8 KB)。當咱們須要讀取表或者索引中的數據時,關係型數據庫會將磁盤中的數據頁映射入存儲緩衝區。當咱們須要修改數據時,關係型數據庫首先會修改內存頁中的數據,而後利用 fsync 這樣的同步工具將改變同步回磁盤中。
不過一旦數據庫突發崩潰,那麼緩衝區中的數據也就丟失,最終打破了事務的持久性。另外一個極端狀況而言,咱們也能夠隨時將數據寫入到磁盤中,可是在崩潰的時候,極可能只寫入了一半的數據,而打破了事務的原子性(Atomicity)。爲了解決這個問題,咱們能夠採起如下兩種方案:
在實際狀況下,Shadow Copies/Pages 會受到極大的磁盤限制,所以絕大部分數據庫仍是選擇了以事務日誌的方式。
爲了實現數據庫狀態的恢復,DBMS 系統一般須要維護事務日誌以追蹤事務中全部影響數據庫數據的操做,以便執行失敗時進行事務的回滾。以 MySQL 的 InnoDB 存儲引擎爲例,InnoDB 存儲引擎經過預寫事務日誌的方式,來保障事務的原子性、一致性以及持久性。它包含 Redo 日誌和 Undo 日誌,Redo 日誌在系統須要的時候,對事務操做進行重作,如當系統宕機重啓後,可以對內存中尚未持久化到磁盤的數據進行恢復,而 Undo 日誌,則可以在事務執行失敗的時候,利用這些 Undo 信息,將數據還原到事務執行前的狀態。
事務日誌能夠提升事務執行的效率,存儲引擎只須要將修改行爲持久到事務日誌當中,即可以只對該數據在內存中的拷貝進行修改,而不須要每次修改都將數據回寫到磁盤。這樣作的好處是,日誌寫入是一小塊區域的順序 I/O,而數據庫數據的磁盤迴寫則是隨機 I/O,磁頭須要不停地移動來尋找須要更新數據的位置,無疑效率更低,經過事務日誌的持久化,既保障了數據存儲的可靠性,又提升了數據寫入的效率。
當某個事務須要去更改數據表中某一行時,未提交的改變會被寫入到內存數據中,而以前的數據會被追加寫入到 Undo Log 文件中。Oracle 或者 MySQL 中使用了所謂 Undo Log 數據結構,而 SQL Server 中則是使用 Transaction Log 完成此項工做。PostgreSQL 並無 Undo Log,不過其內建支持所謂多版本的表數據,即同一行的數據可能同時存在多個版本。總而言之,任何關係型數據庫都採用的相似的數據結構都是爲了容許回滾以及數據的原子性。
某個事務提交以後,內存中的改變就須要同步到磁盤中。不過並非全部的事務提交都會馬上觸發同步,太高頻次的同步反而會對應用性能形成損傷。這裏關係型數據庫就是依靠 Redo Log 來達成這一點,它是一個僅容許追加寫入的基於磁盤的數據結構,它會記錄全部還沒有執行同步的事務操做。相較於一次性寫入固定數目的數據頁到磁盤中,順序地寫入到 Redo Log 會比隨機訪問快上不少。所以,關於事務的 ACID 特性的保證與應用性能之間也就達成了較好的平衡。該數據結構在 Oracle 與 MySQL 中就是叫 Redo Log,而 SQL Server 中則是由 Transaction Log 執行,在 PostgreSQL 中則是使用 Write-Ahead Log(WAL)。下面咱們繼續回到上面的那個問題,應該在什麼時候將內存中的數據寫入到磁盤中。關係型數據庫系統每每使用檢查點來同步內存的髒數據頁與磁盤中的對應部分。爲了不 IO 阻塞,同步過程每每須要等待較長的時間才能完成。所以,關係型數據庫須要保證即便在全部內存髒頁同步到磁盤以前引擎就崩潰的時候不會發生數據丟失。一樣地,在每次數據庫重啓的時候,數據庫引擎會基於 Redo Log 重構那些最後一次成功的檢查點以來全部的內存數據頁。
WAL 協議主要包含了如下三條規則:
一樣能夠參考 PostgreSQL 架構機制 https://url.wx-coder.cn/SgRDQ 中有關於 WAL 的實例討論。