在關系型數(shù)據(jù)庫中同规,事務的重要性不言而喻猜扮,只要對數(shù)據(jù)庫稍有了解的人都知道事務具有 ACID 四個基本屬性救崔,而我們不知道的可能就是數(shù)據(jù)庫是如何實現(xiàn)這四個屬性的苹粟;在這篇文章中飞盆,我們將對事務的實現(xiàn)進行分析恐锦,嘗試理解數(shù)據(jù)庫是如何實現(xiàn)事務的,當然我們也會在文章中簡單對 MySQL 中對 ACID 的實現(xiàn)進行簡單的介紹役听。
事務其實就是并發(fā)控制的基本單位乐严;相信我們都知道衣摩,事務是一個序列操作既琴,其中的操作要么都執(zhí)行磺箕,要么都不執(zhí)行,它是一個不可分割的工作單位谅海;數(shù)據(jù)庫事務的 ACID 四大特性是事務的基礎侥袜,了解了 ACID 是如何實現(xiàn)的颁湖,我們也就清除了事務的實現(xiàn),接下來我們將依次介紹數(shù)據(jù)庫是如何實現(xiàn)這四個特性的惶我。
原子性
在學習事務時,經(jīng)常有人會告訴你声搁,事務就是一系列的操作,要么全部都執(zhí)行窃植,要都不執(zhí)行陵叽,這其實就是對事務原子性的刻畫巩掺;雖然事務具有原子性,但是原子性并不是只與事務有關系,它的身影在很多地方都會出現(xiàn)。
由于操作并不具有原子性累榜,并且可以再分為多個操作,當這些操作出現(xiàn)錯誤或拋出異常時,整個操作就可能不會繼續(xù)執(zhí)行下去猖凛,而已經(jīng)進行的操作造成的副作用就可能造成數(shù)據(jù)更新的丟失或者錯誤进鸠。
事務其實和一個操作沒有什么太大的區(qū)別,它是一系列的數(shù)據(jù)庫操作(可以理解為 SQL)的集合形病,如果事務不具備原子性,那么就沒辦法保證同一個事務中的所有操作都被執(zhí)行或者未被執(zhí)行了霞幅,整個數(shù)據(jù)庫系統(tǒng)就既不可用也不可信漠吻。
回滾日志
想要保證事務的原子性,就需要在異常發(fā)生時司恳,對已經(jīng)執(zhí)行的操作進行回滾途乃,而在 MySQL 中,恢復機制是通過回滾日志(undo log)實現(xiàn)的扔傅,所有事務進行的修改都會先記錄到這個回滾日志中耍共,然后在對數(shù)據(jù)庫中的對應行進行寫入烫饼。
這個過程其實非常好理解,為了能夠在發(fā)生錯誤時撤銷之前的全部操作试读,肯定是需要將之前的操作都記錄下來的杠纵,這樣在發(fā)生錯誤時才可以回滾。
回滾日志除了能夠在發(fā)生錯誤或者用戶執(zhí)行 ROLLBACK
時提供回滾相關的信息钩骇,它還能夠在整個系統(tǒng)發(fā)生崩潰比藻、數(shù)據(jù)庫進程直接被殺死后,當用戶再次啟動數(shù)據(jù)庫進程時倘屹,還能夠立刻通過查詢回滾日志將之前未完成的事務進行回滾银亲,這也就需要回滾日志必須先于數(shù)據(jù)持久化到磁盤上,是我們需要先寫日志后寫數(shù)據(jù)庫的主要原因纽匙。
回滾日志并不能將數(shù)據(jù)庫物理地恢復到執(zhí)行語句或者事務之前的樣子务蝠;它是邏輯日志,當回滾日志被使用時烛缔,它只會按照日志邏輯地將數(shù)據(jù)庫中的修改撤銷掉看馏段,可以理解為,我們在事務中使用的每一條 INSERT
都對應了一條 DELETE
力穗,每一條 UPDATE
也都對應一條相反的 UPDATE
語句毅弧。
在這里,我們并不會介紹回滾日志的格式以及它是如何被管理的当窗,本文重點關注在它到底是一個什么樣的東西够坐,究竟解決了、如何解決了什么樣的問題崖面,如果想要了解具體實現(xiàn)細節(jié)的讀者元咙,相信網(wǎng)絡上關于回滾日志的文章一定不少。
事務的狀態(tài)
因為事務具有原子性巫员,所以從遠處看的話庶香,事務就是密不可分的一個整體,事務的狀態(tài)也只有三種:Active简识、Commited 和 Failed赶掖,事務要不就在執(zhí)行中,要不然就是成功或者失敗的狀態(tài):
但是如果放大來看七扰,我們會發(fā)現(xiàn)事務不再是原子的奢赂,其中包括了很多中間狀態(tài),比如部分提交颈走,事務的狀態(tài)圖也變得越來越復雜膳灶。
事務的狀態(tài)圖以及狀態(tài)的描述取自 Database System Concepts 一書中第 14 章的內容。
- Active:事務的初始狀態(tài),表示事務正在執(zhí)行轧钓;
- Partially Commited:在最后一條語句執(zhí)行之后序厉;
- Failed:發(fā)現(xiàn)事務無法正常執(zhí)行之后;
- Aborted:事務被回滾并且數(shù)據(jù)庫恢復到了事務進行之前的狀態(tài)之后毕箍;
- Commited:成功執(zhí)行整個事務弛房;
雖然在發(fā)生錯誤時,整個數(shù)據(jù)庫的狀態(tài)可以恢復霉晕,但是如果我們在事務中執(zhí)行了諸如:向標準輸出打印日志庭再、向外界發(fā)出郵件、沒有通過數(shù)據(jù)庫修改了磁盤上的內容甚至在事務執(zhí)行期間發(fā)生了轉賬匯款牺堰,那么這些操作作為可見的外部輸出都是沒有辦法回滾的拄轻;這些問題都是由應用開發(fā)者解決和負責的,在絕大多數(shù)情況下伟葫,我們都需要在整個事務提交后恨搓,再觸發(fā)類似的無法回滾的操作。
以訂票為例筏养,哪怕我們在整個事務結束之后斧抱,才向第三方發(fā)起請求,由于向第三方請求并獲取結果是一個需要較長事件的操作渐溶,如果在事務剛剛提交時辉浦,數(shù)據(jù)庫或者服務器發(fā)生了崩潰,那么我們就非常有可能丟失發(fā)起請求這一過程茎辐,這就造成了非常嚴重的問題宪郊;而這一點就不是數(shù)據(jù)庫所能保證的,開發(fā)者需要在適當?shù)臅r候查看請求是否被發(fā)起拖陆、結果是成功還是失敗弛槐。
并行事務的原子性
到目前為止,所有的事務都只是串行執(zhí)行的依啰,一直都沒有考慮過并行執(zhí)行的問題乎串;然而在實際工作中,并行執(zhí)行的事務才是常態(tài)速警,然而并行任務下叹誉,卻可能出現(xiàn)非常復雜的問題:
當 Transaction1 在執(zhí)行的過程中對 id = 1
的用戶進行了讀寫,但是沒有將修改的內容進行提交或者回滾闷旧,在這時 Transaction2 對同樣的數(shù)據(jù)進行了讀操作并提交了事務长豁;也就是說 Transaction2 是依賴于 Transaction1 的,當 Transaction1 由于一些錯誤需要回滾時鸠匀,因為要保證事務的原子性,需要對 Transaction2 進行回滾逾柿,但是由于我們已經(jīng)提交了 Transaction2缀棍,所以我們已經(jīng)沒有辦法進行回滾操作宅此,在這種問題下我們就發(fā)生了問題,Database System Concepts 一書中將這種現(xiàn)象稱為不可恢復安排(Nonrecoverable Schedule)爬范,那什么情況下是可以恢復的呢父腕?
A recoverable schedule is one where, for each pair of transactions Ti and Tj such that Tj reads a data item previously written by Ti , the commit operation of Ti appears before the commit operation of Tj .
簡單理解一下,如果 Transaction2 依賴于事務 Transaction1青瀑,那么事務 Transaction1 必須在 Transaction2 提交之前完成提交的操作:
然而這樣還不算完璧亮,當事務的數(shù)量逐漸增多時,整個恢復流程也會變得越來越復雜斥难,如果我們想要從事務發(fā)生的錯誤中恢復枝嘶,也不是一件那么容易的事情。
在上圖所示的一次事件中哑诊,Transaction2 依賴于 Transaction1群扶,而 Transaction3 又依賴于 Transaction1,當 Transaction1 由于執(zhí)行出現(xiàn)問題發(fā)生回滾時镀裤,為了保證事務的原子性竞阐,就會將 Transaction2 和 Transaction3 中的工作全部回滾,這種情況也叫做級聯(lián)回滾(Cascading Rollback)暑劝,級聯(lián)回滾的發(fā)生會導致大量的工作需要撤回骆莹,是我們難以接受的,不過如果想要達到絕對的原子性担猛,這件事情又是不得不去處理的幕垦,我們會在文章的后面具體介紹如何處理并行事務的原子性。
持久性
既然是數(shù)據(jù)庫毁习,那么一定對數(shù)據(jù)的持久存儲有著非常強烈的需求智嚷,如果數(shù)據(jù)被寫入到數(shù)據(jù)庫中,那么數(shù)據(jù)一定能夠被安全存儲在磁盤上纺且;而事務的持久性就體現(xiàn)在盏道,一旦事務被提交,那么數(shù)據(jù)一定會被寫入到數(shù)據(jù)庫中并持久存儲起來载碌。
當事務已經(jīng)被提交之后猜嘱,就無法再次回滾了,唯一能夠撤回已經(jīng)提交的事務的方式就是創(chuàng)建一個相反的事務對原操作進行『補償』嫁艇,這也是事務持久性的體現(xiàn)之一拥诡。
重做日志
與原子性一樣,事務的持久性也是通過日志來實現(xiàn)的瞒瘸,MySQL 使用重做日志(redo log)實現(xiàn)事務的持久性拳锚,重做日志由兩部分組成,一是內存中的重做日志緩沖區(qū),因為重做日志緩沖區(qū)在內存中点晴,所以它是易失的感凤,另一個就是在磁盤上的重做日志文件,它是持久的粒督。
當我們在一個事務中嘗試對數(shù)據(jù)進行修改時陪竿,它會先將數(shù)據(jù)從磁盤讀入內存,并更新內存中緩存的數(shù)據(jù)屠橄,然后生成一條重做日志并寫入重做日志緩存族跛,當事務真正提交時,MySQL 會將重做日志緩存中的內容刷新到重做日志文件锐墙,再將內存中的數(shù)據(jù)更新到磁盤上礁哄,圖中的第 4、5 步就是在事務提交時執(zhí)行的贮匕。
在 InnoDB 中姐仅,重做日志都是以 512 字節(jié)的塊的形式進行存儲的,同時因為塊的大小與磁盤扇區(qū)大小相同刻盐,所以重做日志的寫入可以保證原子性掏膏,不會由于機器斷電導致重做日志僅寫入一半并留下臟數(shù)據(jù)。
除了所有對數(shù)據(jù)庫的修改會產(chǎn)生重做日志敦锌,因為回滾日志也是需要持久存儲的馒疹,它們也會創(chuàng)建對應的重做日志,在發(fā)生錯誤后乙墙,數(shù)據(jù)庫重啟時會從重做日志中找出未被更新到數(shù)據(jù)庫磁盤中的日志重新執(zhí)行以滿足事務的持久性颖变。
回滾日志和重做日志
到現(xiàn)在為止我們了解了 MySQL 中的兩種日志,回滾日志(undo log)和重做日志(redo log)听想;在數(shù)據(jù)庫系統(tǒng)中腥刹,事務的原子性和持久性是由事務日志(transaction log)保證的,在實現(xiàn)時也就是上面提到的兩種日志汉买,前者用于對事務的影響進行撤銷衔峰,后者在錯誤處理時對已經(jīng)提交的事務進行重做,它們能保證兩點:
- 發(fā)生錯誤或者需要回滾的事務能夠成功回滾(原子性)蛙粘;
- 在事務提交后垫卤,數(shù)據(jù)沒來得及寫會磁盤就宕機時,在下次重新啟動后能夠成功恢復數(shù)據(jù)(持久性)出牧;
在數(shù)據(jù)庫中穴肘,這兩種日志經(jīng)常都是一起工作的,我們可以將它們整體看做一條事務日志舔痕,其中包含了事務的 ID评抚、修改的行元素以及修改前后的值豹缀。
一條事務日志同時包含了修改前后的值,能夠非常簡單的進行回滾和重做兩種操作慨代,在這里我們也不會對重做和回滾日志展開進行介紹耿眉,可能會在之后的文章談一談數(shù)據(jù)庫系統(tǒng)的恢復機制時提到兩種日志的使用。
隔離性
其實作者在之前的文章 『淺入淺出』MySQL 和 InnoDB 就已經(jīng)介紹過數(shù)據(jù)庫事務的隔離性鱼响,不過問了保證文章的獨立性和完整性,我們還會對事務的隔離性進行介紹组底,介紹的內容可能稍微有所不同丈积。
事務的隔離性是數(shù)據(jù)庫處理數(shù)據(jù)的幾大基礎之一,如果沒有數(shù)據(jù)庫的事務之間沒有隔離性债鸡,就會發(fā)生在 并行事務的原子性 一節(jié)中提到的級聯(lián)回滾等問題江滨,造成性能上的巨大損失。如果所有的事務的執(zhí)行順序都是線性的厌均,那么對于事務的管理容易得多唬滑,但是允許事務的并行執(zhí)行卻能能夠提升吞吐量和資源利用率,并且可以減少每個事務的等待時間棺弊。
當多個事務同時并發(fā)執(zhí)行時晶密,事務的隔離性可能就會被違反,雖然單個事務的執(zhí)行可能沒有任何錯誤模她,但是從總體來看就會造成數(shù)據(jù)庫的一致性出現(xiàn)問題稻艰,而串行雖然能夠允許開發(fā)者忽略并行造成的影響,能夠很好地維護數(shù)據(jù)庫的一致性侈净,但是卻會影響事務執(zhí)行的性能尊勿。
事務的隔離級別
所以說數(shù)據(jù)庫的隔離性和一致性其實是一個需要開發(fā)者去權衡的問題,為數(shù)據(jù)庫提供什么樣的隔離性層級也就決定了數(shù)據(jù)庫的性能以及可以達到什么樣的一致性畜侦;在 SQL 標準中定義了四種數(shù)據(jù)庫的事務的隔離級別:READ UNCOMMITED
元扔、READ COMMITED
、REPEATABLE READ
和 SERIALIZABLE
旋膳;每個事務的隔離級別其實都比上一級多解決了一個問題:
-
RAED UNCOMMITED
:使用查詢語句不會加鎖澎语,可能會讀到未提交的行(Dirty Read); -
READ COMMITED
:只對記錄加記錄鎖溺忧,而不會在記錄之間加間隙鎖咏连,所以允許新的記錄插入到被鎖定記錄的附近,所以再多次使用查詢語句時鲁森,可能得到不同的結果(Non-Repeatable Read)祟滴; -
REPEATABLE READ
:多次讀取同一范圍的數(shù)據(jù)會返回第一次查詢的快照,不會返回不同的數(shù)據(jù)行歌溉,但是可能發(fā)生幻讀(Phantom Read)垄懂; -
SERIALIZABLE
:InnoDB 隱式地將全部的查詢語句加上共享鎖骑晶,解決了幻讀的問題;
以上的所有的事務隔離級別都不允許臟寫入(Dirty Write)草慧,也就是當前事務更新了另一個事務已經(jīng)更新但是還未提交的數(shù)據(jù)桶蛔,大部分的數(shù)據(jù)庫中都使用了 READ COMMITED 作為默認的事務隔離級別,但是 MySQL 使用了 REPEATABLE READ 作為默認配置漫谷;從 RAED UNCOMMITED 到 SERIALIZABLE仔雷,隨著事務隔離級別變得越來越嚴格,數(shù)據(jù)庫對于并發(fā)執(zhí)行事務的性能也逐漸下降舔示。
對于數(shù)據(jù)庫的使用者碟婆,從理論上說,并不需要知道事務的隔離級別是如何實現(xiàn)的惕稻,我們只需要知道這個隔離級別解決了什么樣的問題竖共,但是不同數(shù)據(jù)庫對于不同隔離級別的是實現(xiàn)細節(jié)在很多時候都會讓我們遇到意料之外的坑。
如果讀者不了解臟讀俺祠、不可重復讀和幻讀究竟是什么公给,可以閱讀之前的文章 『淺入淺出』MySQL 和 InnoDB,在這里我們僅放一張圖來展示各個隔離層級對這幾個問題的解決情況蜘渣。
隔離級別的實現(xiàn)
數(shù)據(jù)庫對于隔離級別的實現(xiàn)就是使用并發(fā)控制機制對在同一時間執(zhí)行的事務進行控制淌铐,限制不同的事務對于同一資源的訪問和更新,而最重要也最常見的并發(fā)控制機制蔫缸,在這里我們將簡單介紹三種最重要的并發(fā)控制器機制的工作原理匣沼。
鎖
鎖是一種最為常見的并發(fā)控制機制,在一個事務中捂龄,我們并不會將整個數(shù)據(jù)庫都加鎖释涛,而是只會鎖住那些需要訪問的數(shù)據(jù)項, MySQL 和常見數(shù)據(jù)庫中的鎖都分為兩種倦沧,共享鎖(Shared)和互斥鎖(Exclusive)唇撬,前者也叫讀鎖,后者叫寫鎖展融。
讀鎖保證了讀操作可以并發(fā)執(zhí)行窖认,相互不會影響,而寫鎖保證了在更新數(shù)據(jù)庫數(shù)據(jù)時不會有其他的事務訪問或者更改同一條記錄造成不可預知的問題告希。
時間戳
除了鎖扑浸,另一種實現(xiàn)事務的隔離性的方式就是通過時間戳,使用這種方式實現(xiàn)事務的數(shù)據(jù)庫燕偶,例如 PostgreSQL 會為每一條記錄保留兩個字段喝噪;讀時間戳中報錯了所有訪問該記錄的事務中的最大時間戳,而記錄行的寫時間戳中保存了將記錄改到當前值的事務的時間戳指么。
使用時間戳實現(xiàn)事務的隔離性時酝惧,往往都會使用樂觀鎖榴鼎,先對數(shù)據(jù)進行修改,在寫回時再去判斷當前值晚唇,也就是時間戳是否改變過巫财,如果沒有改變過,就寫入哩陕,否則平项,生成一個新的時間戳并再次更新數(shù)據(jù),樂觀鎖其實并不是真正的鎖機制悍及,它只是一種思想葵礼,在這里并不會對它進行展開介紹。
多版本和快照隔離
通過維護多個版本的數(shù)據(jù)并鸵,數(shù)據(jù)庫可以允許事務在數(shù)據(jù)被其他事務更新時對舊版本的數(shù)據(jù)進行讀取,很多數(shù)據(jù)庫都對這一機制進行了實現(xiàn)扔涧;因為所有的讀操作不再需要等待寫鎖的釋放园担,所以能夠顯著地提升讀的性能,MySQL 和 PostgreSQL 都對這一機制進行自己的實現(xiàn)枯夜,也就是 MVCC弯汰,雖然各自實現(xiàn)的方式有所不同,MySQL 就通過文章中提到的回滾日志實現(xiàn)了 MVCC湖雹,保證事務并行執(zhí)行時能夠不等待互斥鎖的釋放直接獲取數(shù)據(jù)咏闪。
隔離性與原子性
在這里就需要簡單提一下在在原子性一節(jié)中遇到的級聯(lián)回滾等問題了,如果一個事務對數(shù)據(jù)進行了寫入摔吏,這時就會獲取一個互斥鎖鸽嫂,其他的事務就想要獲得改行數(shù)據(jù)的讀鎖就必須等待寫鎖的釋放,自然就不會發(fā)生級聯(lián)回滾等問題了征讲。
不過在大多數(shù)的數(shù)據(jù)庫据某,比如 MySQL 中都使用了 MVCC 等特性,也就是正常的讀方法是不需要獲取鎖的诗箍,在想要對讀取的數(shù)據(jù)進行更新時需要使用 SELECT ... FOR UPDATE
嘗試獲取對應行的互斥鎖癣籽,以保證不同事務可以正常工作。
一致性
作者認為數(shù)據(jù)庫的一致性是一個非常讓人迷惑的概念滤祖,原因是數(shù)據(jù)庫領域其實包含兩個一致性筷狼,一個是 ACID 中的一致性、另一個是 CAP 定義中的一致性匠童。
這兩個數(shù)據(jù)庫的一致性說的完全不是一個事情埂材,很多很多人都對這兩者的概念有非常深的誤解,當我們在討論數(shù)據(jù)庫的一致性時汤求,一定要清楚上下文的語義是什么楞遏,盡量明確的問出我們要討論的到底是 ACID 中的一致性還是 CAP 中的一致性茬暇。
ACID
數(shù)據(jù)庫對于 ACID 中的一致性的定義是這樣的:如果一個事務原子地在一個一致地數(shù)據(jù)庫中獨立運行,那么在它執(zhí)行之后寡喝,數(shù)據(jù)庫的狀態(tài)一定是一致的糙俗。對于這個概念,它的第一層意思就是對于數(shù)據(jù)完整性的約束预鬓,包括主鍵約束巧骚、引用約束以及一些約束檢查等等,在事務的執(zhí)行的前后以及過程中不會違背對數(shù)據(jù)完整性的約束格二,所有對數(shù)據(jù)庫寫入的操作都應該是合法的劈彪,并不能產(chǎn)生不合法的數(shù)據(jù)狀態(tài)。
A transaction must preserve database consistency - if a transaction is run atomically in isolation starting from a consistent database, the database must again be consistent at the end of the transaction.
我們可以將事務理解成一個函數(shù)顶猜,它接受一個外界的 SQL 輸入和一個一致的數(shù)據(jù)庫沧奴,它一定會返回一個一致的數(shù)據(jù)庫。
而第二層意思其實是指邏輯上的對于開發(fā)者的要求长窄,我們要在代碼中寫出正確的事務邏輯滔吠,比如銀行轉賬,事務中的邏輯不可能只扣錢或者只加錢挠日,這是應用層面上對于數(shù)據(jù)庫一致性的要求疮绷。
Ensuring consistency for an individual transaction is the responsibility of the application programmer who codes the transaction. - Database System Concepts
數(shù)據(jù)庫 ACID 中的一致性對事務的要求不止包含對數(shù)據(jù)完整性以及合法性的檢查,還包含應用層面邏輯的正確嚣潜。
CAP 定理中的數(shù)據(jù)一致性冬骚,其實是說分布式系統(tǒng)中的各個節(jié)點中對于同一數(shù)據(jù)的拷貝有著相同的值;而 ACID 中的一致性是指數(shù)據(jù)庫的規(guī)則懂算,如果 schema 中規(guī)定了一個值必須是唯一的只冻,那么一致的系統(tǒng)必須確保在所有的操作中,該值都是唯一的计技,由此來看 CAP 和 ACID 對于一致性的定義有著根本性的區(qū)別属愤。
總結
事務的 ACID 四大基本特性是保證數(shù)據(jù)庫能夠運行的基石,但是完全保證數(shù)據(jù)庫的 ACID酸役,尤其是隔離性會對性能有比較大影響住诸,在實際的使用中我們也會根據(jù)業(yè)務的需求對隔離性進行調整,除了隔離性涣澡,數(shù)據(jù)庫的原子性和持久性相信都是比較好理解的特性贱呐,前者保證數(shù)據(jù)庫的事務要么全部執(zhí)行、要么全部不執(zhí)行入桂,后者保證了對數(shù)據(jù)庫的寫入都是持久存儲的奄薇、非易失的,而一致性不僅是數(shù)據(jù)庫對本身數(shù)據(jù)的完整性的要求抗愁,同時也對開發(fā)者提出了要求 - 寫出邏輯正確并且合理的事務馁蒂。
最后呵晚,也是最重要的,當別人在將一致性的時候沫屡,一定要搞清楚他的上下文饵隙,如果對文章的內容有疑問,可以在評論中留言沮脖,評論系統(tǒng)使用 Disqus 需要梯子金矛。
Follow: Draveness · GitHub
References
- Database System Concepts
- 數(shù)據(jù)庫事務
- How does MVCC (Multi-Version Concurrency Control) work
- How does a relational database work
- Implementing Transaction Processing using Redo Logs
- Implementing Transaction Processing using Undo Logs
- Undo/Redo Logging Rules
- MySQL 解密:InnoDB 存儲引擎重做日志漫游
- ACID 中 C 與 CAP 定理中 C 的區(qū)別
- Disambiguating ACID and CAP