本文轉(zhuǎn)自互聯(lián)網(wǎng)
本系列文章將整理到我在GitHub上的《Java面試指南》倉庫视事,更多精彩內(nèi)容請到我的倉庫里查看
喜歡的話麻煩點(diǎn)下Star哈
文章首發(fā)于我的個人博客:
本文是微信公眾號【Java技術(shù)江湖】的《重新學(xué)習(xí)MySQL數(shù)據(jù)庫》其中一篇,本文部分內(nèi)容來源于網(wǎng)絡(luò)苛白,為了把本文主題講得清晰透徹藐唠,也整合了很多我認(rèn)為不錯的技術(shù)博客內(nèi)容,引用其中了一些比較好的博客文章宦棺,如有侵權(quán)舌缤,請聯(lián)系作者箕戳。
該系列博文會告訴你如何從入門到進(jìn)階,從sql基本的使用方法国撵,從MySQL執(zhí)行引擎再到索引陵吸、事務(wù)等知識,一步步地學(xué)習(xí)MySQL相關(guān)技術(shù)的實(shí)現(xiàn)原理卸留,更好地了解如何基于這些知識來優(yōu)化sql走越,減少SQL執(zhí)行時間,通過執(zhí)行計(jì)劃對SQL性能進(jìn)行分析耻瑟,再到MySQL的主從復(fù)制旨指、主備部署等內(nèi)容,以便讓你更完整地了解整個MySQL方面的技術(shù)體系喳整,形成自己的知識框架谆构。
如果對本系列文章有什么建議,或者是有什么疑問的話框都,也可以關(guān)注公眾號【Java技術(shù)江湖】聯(lián)系作者搬素,歡迎你參與本系列博文的創(chuàng)作和修訂。
『淺入深出』MySQL 中事務(wù)的實(shí)現(xiàn)
在關(guān)系型數(shù)據(jù)庫中,事務(wù)的重要性不言而喻熬尺,只要對數(shù)據(jù)庫稍有了解的人都知道事務(wù)具有 ACID 四個基本屬性摸屠,而我們不知道的可能就是數(shù)據(jù)庫是如何實(shí)現(xiàn)這四個屬性的;在這篇文章中粱哼,我們將對事務(wù)的實(shí)現(xiàn)進(jìn)行分析季二,嘗試?yán)斫鈹?shù)據(jù)庫是如何實(shí)現(xiàn)事務(wù)的,當(dāng)然我們也會在文章中簡單對 MySQL 中對 ACID 的實(shí)現(xiàn)進(jìn)行簡單的介紹揭措。
事務(wù)其實(shí)就是并發(fā)控制的基本單位胯舷;相信我們都知道,事務(wù)是一個序列操作绊含,其中的操作要么都執(zhí)行桑嘶,要么都不執(zhí)行,它是一個不可分割的工作單位躬充;數(shù)據(jù)庫事務(wù)的 ACID 四大特性是事務(wù)的基礎(chǔ)逃顶,了解了 ACID 是如何實(shí)現(xiàn)的,我們也就清除了事務(wù)的實(shí)現(xiàn)充甚,接下來我們將依次介紹數(shù)據(jù)庫是如何實(shí)現(xiàn)這四個特性的口蝠。
原子性
在學(xué)習(xí)事務(wù)時,經(jīng)常有人會告訴你津坑,事務(wù)就是一系列的操作,要么全部都執(zhí)行傲霸,要都不執(zhí)行疆瑰,這其實(shí)就是對事務(wù)原子性的刻畫;雖然事務(wù)具有原子性昙啄,但是原子性并不是只與事務(wù)有關(guān)系穆役,它的身影在很多地方都會出現(xiàn)。
由于操作并不具有原子性梳凛,并且可以再分為多個操作耿币,當(dāng)這些操作出現(xiàn)錯誤或拋出異常時,整個操作就可能不會繼續(xù)執(zhí)行下去韧拒,而已經(jīng)進(jìn)行的操作造成的副作用就可能造成數(shù)據(jù)更新的丟失或者錯誤淹接。
事務(wù)其實(shí)和一個操作沒有什么太大的區(qū)別,它是一系列的數(shù)據(jù)庫操作(可以理解為 SQL)的集合叛溢,如果事務(wù)不具備原子性塑悼,那么就沒辦法保證同一個事務(wù)中的所有操作都被執(zhí)行或者未被執(zhí)行了,整個數(shù)據(jù)庫系統(tǒng)就既不可用也不可信楷掉。
回滾日志
想要保證事務(wù)的原子性厢蒜,就需要在異常發(fā)生時,對已經(jīng)執(zhí)行的操作進(jìn)行回滾,而在 MySQL 中斑鸦,恢復(fù)機(jī)制是通過回滾日志(undo log)實(shí)現(xiàn)的愕贡,所有事務(wù)進(jìn)行的修改都會先記錄到這個回滾日志中,然后在對數(shù)據(jù)庫中的對應(yīng)行進(jìn)行寫入巷屿。
這個過程其實(shí)非常好理解固以,為了能夠在發(fā)生錯誤時撤銷之前的全部操作,肯定是需要將之前的操作都記錄下來的攒庵,這樣在發(fā)生錯誤時才可以回滾嘴纺。
回滾日志除了能夠在發(fā)生錯誤或者用戶執(zhí)行 ROLLBACK 時提供回滾相關(guān)的信息,它還能夠在整個系統(tǒng)發(fā)生崩潰浓冒、數(shù)據(jù)庫進(jìn)程直接被殺死后栽渴,當(dāng)用戶再次啟動數(shù)據(jù)庫進(jìn)程時,還能夠立刻通過查詢回滾日志將之前未完成的事務(wù)進(jìn)行回滾稳懒,這也就需要回滾日志必須先于數(shù)據(jù)持久化到磁盤上闲擦,是我們需要先寫日志后寫數(shù)據(jù)庫的主要原因。
回滾日志并不能將數(shù)據(jù)庫物理地恢復(fù)到執(zhí)行語句或者事務(wù)之前的樣子场梆;它是邏輯日志墅冷,當(dāng)回滾日志被使用時,它只會按照日志邏輯地將數(shù)據(jù)庫中的修改撤銷掉看或油,可以理解為寞忿,我們在事務(wù)中使用的每一條 INSERT 都對應(yīng)了一條 DELETE,每一條 UPDATE 也都對應(yīng)一條相反的 UPDATE 語句顶岸。
在這里腔彰,我們并不會介紹回滾日志的格式以及它是如何被管理的,本文重點(diǎn)關(guān)注在它到底是一個什么樣的東西辖佣,究竟解決了霹抛、如何解決了什么樣的問題,如果想要了解具體實(shí)現(xiàn)細(xì)節(jié)的讀者卷谈,相信網(wǎng)絡(luò)上關(guān)于回滾日志的文章一定不少杯拐。
事務(wù)的狀態(tài)
因?yàn)槭聞?wù)具有原子性,所以從遠(yuǎn)處看的話世蔗,事務(wù)就是密不可分的一個整體端逼,事務(wù)的狀態(tài)也只有三種:Active、Commited 和 Failed污淋,事務(wù)要不就在執(zhí)行中裳食,要不然就是成功或者失敗的狀態(tài):
但是如果放大來看,我們會發(fā)現(xiàn)事務(wù)不再是原子的芙沥,其中包括了很多中間狀態(tài)诲祸,比如部分提交浊吏,事務(wù)的狀態(tài)圖也變得越來越復(fù)雜。
事務(wù)的狀態(tài)圖以及狀態(tài)的描述取自 Database System Concepts 一書中第 14 章的內(nèi)容救氯。
Active:事務(wù)的初始狀態(tài)找田,表示事務(wù)正在執(zhí)行;
Partially Commited:在最后一條語句執(zhí)行之后着憨;
Failed:發(fā)現(xiàn)事務(wù)無法正常執(zhí)行之后墩衙;
Aborted:事務(wù)被回滾并且數(shù)據(jù)庫恢復(fù)到了事務(wù)進(jìn)行之前的狀態(tài)之后;
Commited:成功執(zhí)行整個事務(wù)甲抖;
雖然在發(fā)生錯誤時漆改,整個數(shù)據(jù)庫的狀態(tài)可以恢復(fù),但是如果我們在事務(wù)中執(zhí)行了諸如:向標(biāo)準(zhǔn)輸出打印日志准谚、向外界發(fā)出郵件挫剑、沒有通過數(shù)據(jù)庫修改了磁盤上的內(nèi)容甚至在事務(wù)執(zhí)行期間發(fā)生了轉(zhuǎn)賬匯款,那么這些操作作為可見的外部輸出都是沒有辦法回滾的柱衔;這些問題都是由應(yīng)用開發(fā)者解決和負(fù)責(zé)的樊破,在絕大多數(shù)情況下,我們都需要在整個事務(wù)提交后唆铐,再觸發(fā)類似的無法回滾的操作
以訂票為例哲戚,哪怕我們在整個事務(wù)結(jié)束之后,才向第三方發(fā)起請求艾岂,由于向第三方請求并獲取結(jié)果是一個需要較長事件的操作顺少,如果在事務(wù)剛剛提交時,數(shù)據(jù)庫或者服務(wù)器發(fā)生了崩潰王浴,那么我們就非常有可能丟失發(fā)起請求這一過程祈纯,這就造成了非常嚴(yán)重的問題;而這一點(diǎn)就不是數(shù)據(jù)庫所能保證的叼耙,開發(fā)者需要在適當(dāng)?shù)臅r候查看請求是否被發(fā)起、結(jié)果是成功還是失敗粒没。
并行事務(wù)的原子性
到目前為止筛婉,所有的事務(wù)都只是串行執(zhí)行的,一直都沒有考慮過并行執(zhí)行的問題癞松;然而在實(shí)際工作中爽撒,并行執(zhí)行的事務(wù)才是常態(tài),然而并行任務(wù)下响蓉,卻可能出現(xiàn)非常復(fù)雜的問題:
當(dāng) Transaction1 在執(zhí)行的過程中對 id = 1 的用戶進(jìn)行了讀寫硕勿,但是沒有將修改的內(nèi)容進(jìn)行提交或者回滾,在這時 Transaction2 對同樣的數(shù)據(jù)進(jìn)行了讀操作并提交了事務(wù)枫甲;也就是說 Transaction2 是依賴于 Transaction1 的源武,當(dāng) Transaction1 由于一些錯誤需要回滾時扼褪,因?yàn)橐WC事務(wù)的原子性,需要對 Transaction2 進(jìn)行回滾粱栖,但是由于我們已經(jīng)提交了 Transaction2话浇,所以我們已經(jīng)沒有辦法進(jìn)行回滾操作,在這種問題下我們就發(fā)生了問題闹究,Database System Concepts 一書中將這種現(xiàn)象稱為不可恢復(fù)安排(Nonrecoverable Schedule)幔崖,那什么情況下是可以恢復(fù)的呢?
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 依賴于事務(wù) Transaction1赏寇,那么事務(wù) Transaction1 必須在 Transaction2 提交之前完成提交的操作:
然而這樣還不算完,當(dāng)事務(wù)的數(shù)量逐漸增多時价认,整個恢復(fù)流程也會變得越來越復(fù)雜嗅定,如果我們想要從事務(wù)發(fā)生的錯誤中恢復(fù),也不是一件那么容易的事情刻伊。
在上圖所示的一次事件中露戒,Transaction2 依賴于 Transaction1,而 Transaction3 又依賴于 Transaction1捶箱,當(dāng) Transaction1 由于執(zhí)行出現(xiàn)問題發(fā)生回滾時智什,為了保證事務(wù)的原子性,就會將 Transaction2 和 Transaction3 中的工作全部回滾丁屎,這種情況也叫做級聯(lián)回滾(Cascading Rollback)荠锭,級聯(lián)回滾的發(fā)生會導(dǎo)致大量的工作需要撤回,是我們難以接受的晨川,不過如果想要達(dá)到絕對的原子性证九,這件事情又是不得不去處理的,我們會在文章的后面具體介紹如何處理并行事務(wù)的原子性共虑。
持久性
既然是數(shù)據(jù)庫愧怜,那么一定對數(shù)據(jù)的持久存儲有著非常強(qiáng)烈的需求,如果數(shù)據(jù)被寫入到數(shù)據(jù)庫中妈拌,那么數(shù)據(jù)一定能夠被安全存儲在磁盤上拥坛;而事務(wù)的持久性就體現(xiàn)在,一旦事務(wù)被提交尘分,那么數(shù)據(jù)一定會被寫入到數(shù)據(jù)庫中并持久存儲起來猜惋。
當(dāng)事務(wù)已經(jīng)被提交之后,就無法再次回滾了培愁,唯一能夠撤回已經(jīng)提交的事務(wù)的方式就是創(chuàng)建一個相反的事務(wù)對原操作進(jìn)行『補(bǔ)償』著摔,這也是事務(wù)持久性的體現(xiàn)之一。
重做日志
與原子性一樣定续,事務(wù)的持久性也是通過日志來實(shí)現(xiàn)的谍咆,MySQL 使用重做日志(redo log)實(shí)現(xiàn)事務(wù)的持久性禾锤,重做日志由兩部分組成,一是內(nèi)存中的重做日志緩沖區(qū)卧波,因?yàn)橹刈鋈罩揪彌_區(qū)在內(nèi)存中时肿,所以它是易失的,另一個就是在磁盤上的重做日志文件港粱,它是持久的
當(dāng)我們在一個事務(wù)中嘗試對數(shù)據(jù)進(jìn)行修改時螃成,它會先將數(shù)據(jù)從磁盤讀入內(nèi)存,并更新內(nèi)存中緩存的數(shù)據(jù)查坪,然后生成一條重做日志并寫入重做日志緩存寸宏,當(dāng)事務(wù)真正提交時,MySQL 會將重做日志緩存中的內(nèi)容刷新到重做日志文件偿曙,再將內(nèi)存中的數(shù)據(jù)更新到磁盤上氮凝,圖中的第 4、5 步就是在事務(wù)提交時執(zhí)行的望忆。
在 InnoDB 中罩阵,重做日志都是以 512 字節(jié)的塊的形式進(jìn)行存儲的,同時因?yàn)閴K的大小與磁盤扇區(qū)大小相同启摄,所以重做日志的寫入可以保證原子性惩猫,不會由于機(jī)器斷電導(dǎo)致重做日志僅寫入一半并留下臟數(shù)據(jù)检号。
除了所有對數(shù)據(jù)庫的修改會產(chǎn)生重做日志,因?yàn)榛貪L日志也是需要持久存儲的,它們也會創(chuàng)建對應(yīng)的重做日志纤壁,在發(fā)生錯誤后轿亮,數(shù)據(jù)庫重啟時會從重做日志中找出未被更新到數(shù)據(jù)庫磁盤中的日志重新執(zhí)行以滿足事務(wù)的持久性空凸。
回滾日志和重做日志
到現(xiàn)在為止我們了解了 MySQL 中的兩種日志避矢,回滾日志(undo log)和重做日志(redo log);在數(shù)據(jù)庫系統(tǒng)中龟再,事務(wù)的原子性和持久性是由事務(wù)日志(transaction log)保證的书闸,在實(shí)現(xiàn)時也就是上面提到的兩種日志,前者用于對事務(wù)的影響進(jìn)行撤銷利凑,后者在錯誤處理時對已經(jīng)提交的事務(wù)進(jìn)行重做浆劲,它們能保證兩點(diǎn):
發(fā)生錯誤或者需要回滾的事務(wù)能夠成功回滾(原子性);
在事務(wù)提交后截碴,數(shù)據(jù)沒來得及寫會磁盤就宕機(jī)時,在下次重新啟動后能夠成功恢復(fù)數(shù)據(jù)(持久性)蛉威;
在數(shù)據(jù)庫中日丹,這兩種日志經(jīng)常都是一起工作的,我們可以將它們整體看做一條事務(wù)日志蚯嫌,其中包含了事務(wù)的 ID哲虾、修改的行元素以及修改前后的值丙躏。
一條事務(wù)日志同時包含了修改前后的值,能夠非常簡單的進(jìn)行回滾和重做兩種操作束凑,在這里我們也不會對重做和回滾日志展開進(jìn)行介紹晒旅,可能會在之后的文章談一談數(shù)據(jù)庫系統(tǒng)的恢復(fù)機(jī)制時提到兩種日志的使用。
隔離性
其實(shí)作者在之前的文章 『淺入淺出』MySQL 和 InnoDB 就已經(jīng)介紹過數(shù)據(jù)庫事務(wù)的隔離性汪诉,不過為了保證文章的獨(dú)立性和完整性废恋,我們還會對事務(wù)的隔離性進(jìn)行介紹,介紹的內(nèi)容可能稍微有所不同扒寄。
事務(wù)的隔離性是數(shù)據(jù)庫處理數(shù)據(jù)的幾大基礎(chǔ)之一鱼鼓,如果沒有數(shù)據(jù)庫的事務(wù)之間沒有隔離性,就會發(fā)生在 并行事務(wù)的原子性 一節(jié)中提到的級聯(lián)回滾等問題该编,造成性能上的巨大損失迄本。如果所有的事務(wù)的執(zhí)行順序都是線性的,那么對于事務(wù)的管理容易得多课竣,但是允許事務(wù)的并行執(zhí)行卻能能夠提升吞吐量和資源利用率嘉赎,并且可以減少每個事務(wù)的等待時間。
當(dāng)多個事務(wù)同時并發(fā)執(zhí)行時于樟,事務(wù)的隔離性可能就會被違反公条,雖然單個事務(wù)的執(zhí)行可能沒有任何錯誤,但是從總體來看就會造成數(shù)據(jù)庫的一致性出現(xiàn)問題隔披,而串行雖然能夠允許開發(fā)者忽略并行造成的影響赃份,能夠很好地維護(hù)數(shù)據(jù)庫的一致性,但是卻會影響事務(wù)執(zhí)行的性能奢米。
事務(wù)的隔離級別
所以說數(shù)據(jù)庫的隔離性和一致性其實(shí)是一個需要開發(fā)者去權(quán)衡的問題抓韩,為數(shù)據(jù)庫提供什么樣的隔離性層級也就決定了數(shù)據(jù)庫的性能以及可以達(dá)到什么樣的一致性;在 SQL 標(biāo)準(zhǔn)中定義了四種數(shù)據(jù)庫的事務(wù)的隔離級別:READ UNCOMMITED鬓长、READ COMMITED谒拴、REPEATABLE READ 和 SERIALIZABLE;每個事務(wù)的隔離級別其實(shí)都比上一級多解決了一個問題:
RAED UNCOMMITED:使用查詢語句不會加鎖涉波,可能會讀到未提交的行(Dirty Read)英上;
READ COMMITED:只對記錄加記錄鎖,而不會在記錄之間加間隙鎖啤覆,所以允許新的記錄插入到被鎖定記錄的附近苍日,所以再多次使用查詢語句時,可能得到不同的結(jié)果(Non-Repeatable Read)窗声;
REPEATABLE READ:多次讀取同一范圍的數(shù)據(jù)會返回第一次查詢的快照相恃,不會返回不同的數(shù)據(jù)行,但是可能發(fā)生幻讀(Phantom Read)笨觅;
SERIALIZABLE:InnoDB 隱式地將全部的查詢語句加上共享鎖拦耐,解決了幻讀的問題耕腾;
以上的所有的事務(wù)隔離級別都不允許臟寫入(Dirty Write),也就是當(dāng)前事務(wù)更新了另一個事務(wù)已經(jīng)更新但是還未提交的數(shù)據(jù)杀糯,大部分的數(shù)據(jù)庫中都使用了 READ COMMITED 作為默認(rèn)的事務(wù)隔離級別扫俺,但是 MySQL 使用了 REPEATABLE READ 作為默認(rèn)配置;從 RAED UNCOMMITED 到 SERIALIZABLE固翰,隨著事務(wù)隔離級別變得越來越嚴(yán)格狼纬,數(shù)據(jù)庫對于并發(fā)執(zhí)行事務(wù)的性能也逐漸下降。
對于數(shù)據(jù)庫的使用者倦挂,從理論上說畸颅,并不需要知道事務(wù)的隔離級別是如何實(shí)現(xiàn)的,我們只需要知道這個隔離級別解決了什么樣的問題方援,但是不同數(shù)據(jù)庫對于不同隔離級別的是實(shí)現(xiàn)細(xì)節(jié)在很多時候都會讓我們遇到意料之外的坑没炒。
如果讀者不了解臟讀、不可重復(fù)讀和幻讀究竟是什么犯戏,可以閱讀之前的文章 『淺入淺出』MySQL 和 InnoDB送火,在這里我們僅放一張圖來展示各個隔離層級對這幾個問題的解決情況。
隔離級別的實(shí)現(xiàn)
數(shù)據(jù)庫對于隔離級別的實(shí)現(xiàn)就是使用并發(fā)控制機(jī)制對在同一時間執(zhí)行的事務(wù)進(jìn)行控制先匪,限制不同的事務(wù)對于同一資源的訪問和更新种吸,而最重要也最常見的并發(fā)控制機(jī)制,在這里我們將簡單介紹三種最重要的并發(fā)控制器機(jī)制的工作原理呀非。
鎖
鎖是一種最為常見的并發(fā)控制機(jī)制坚俗,在一個事務(wù)中,我們并不會將整個數(shù)據(jù)庫都加鎖岸裙,而是只會鎖住那些需要訪問的數(shù)據(jù)項(xiàng)猖败, MySQL 和常見數(shù)據(jù)庫中的鎖都分為兩種,共享鎖(Shared)和互斥鎖(Exclusive)降允,前者也叫讀鎖恩闻,后者叫寫鎖。
讀鎖保證了讀操作可以并發(fā)執(zhí)行剧董,相互不會影響幢尚,而寫鎖保證了在更新數(shù)據(jù)庫數(shù)據(jù)時不會有其他的事務(wù)訪問或者更改同一條記錄造成不可預(yù)知的問題。
時間戳
除了鎖翅楼,另一種實(shí)現(xiàn)事務(wù)的隔離性的方式就是通過時間戳尉剩,使用這種方式實(shí)現(xiàn)事務(wù)的數(shù)據(jù)庫,例如 PostgreSQL 會為每一條記錄保留兩個字段毅臊;讀時間戳中報(bào)錯了所有訪問該記錄的事務(wù)中的最大時間戳理茎,而記錄行的寫時間戳中保存了將記錄改到當(dāng)前值的事務(wù)的時間戳。
使用時間戳實(shí)現(xiàn)事務(wù)的隔離性時,往往都會使用樂觀鎖功蜓,先對數(shù)據(jù)進(jìn)行修改,在寫回時再去判斷當(dāng)前值宠蚂,也就是時間戳是否改變過式撼,如果沒有改變過,就寫入求厕,否則著隆,生成一個新的時間戳并再次更新數(shù)據(jù),樂觀鎖其實(shí)并不是真正的鎖機(jī)制呀癣,它只是一種思想美浦,在這里并不會對它進(jìn)行展開介紹。
多版本和快照隔離
通過維護(hù)多個版本的數(shù)據(jù)项栏,數(shù)據(jù)庫可以允許事務(wù)在數(shù)據(jù)被其他事務(wù)更新時對舊版本的數(shù)據(jù)進(jìn)行讀取浦辨,很多數(shù)據(jù)庫都對這一機(jī)制進(jìn)行了實(shí)現(xiàn);因?yàn)樗械淖x操作不再需要等待寫鎖的釋放沼沈,所以能夠顯著地提升讀的性能流酬,MySQL 和 PostgreSQL 都對這一機(jī)制進(jìn)行自己的實(shí)現(xiàn),也就是 MVCC列另,雖然各自實(shí)現(xiàn)的方式有所不同芽腾,MySQL 就通過文章中提到的回滾日志實(shí)現(xiàn)了 MVCC,保證事務(wù)并行執(zhí)行時能夠不等待互斥鎖的釋放直接獲取數(shù)據(jù)页衙。
隔離性與原子性
在這里就需要簡單提一下在在原子性一節(jié)中遇到的級聯(lián)回滾等問題了摊滔,如果一個事務(wù)對數(shù)據(jù)進(jìn)行了寫入,這時就會獲取一個互斥鎖店乐,其他的事務(wù)就想要獲得改行數(shù)據(jù)的讀鎖就必須等待寫鎖的釋放艰躺,自然就不會發(fā)生級聯(lián)回滾等問題了。
不過在大多數(shù)的數(shù)據(jù)庫响巢,比如 MySQL 中都使用了 MVCC 等特性描滔,也就是正常的讀方法是不需要獲取鎖的,在想要對讀取的數(shù)據(jù)進(jìn)行更新時需要使用 SELECT ... FOR UPDATE 嘗試獲取對應(yīng)行的互斥鎖踪古,以保證不同事務(wù)可以正常工作含长。
一致性
作者認(rèn)為數(shù)據(jù)庫的一致性是一個非常讓人迷惑的概念,原因是數(shù)據(jù)庫領(lǐng)域其實(shí)包含兩個一致性伏穆,一個是 ACID 中的一致性拘泞、另一個是 CAP 定義中的一致性。
這兩個數(shù)據(jù)庫的一致性說的完全不是一個事情枕扫,很多很多人都對這兩者的概念有非常深的誤解陪腌,當(dāng)我們在討論數(shù)據(jù)庫的一致性時,一定要清楚上下文的語義是什么,盡量明確的問出我們要討論的到底是 ACID 中的一致性還是 CAP 中的一致性诗鸭。
ACID
數(shù)據(jù)庫對于 ACID 中的一致性的定義是這樣的:如果一個事務(wù)原子地在一個一致地?cái)?shù)據(jù)庫中獨(dú)立運(yùn)行染簇,那么在它執(zhí)行之后,數(shù)據(jù)庫的狀態(tài)一定是一致的强岸。對于這個概念锻弓,它的第一層意思就是對于數(shù)據(jù)完整性的約束,包括主鍵約束蝌箍、引用約束以及一些約束檢查等等青灼,在事務(wù)的執(zhí)行的前后以及過程中不會違背對數(shù)據(jù)完整性的約束,所有對數(shù)據(jù)庫寫入的操作都應(yīng)該是合法的妓盲,并不能產(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.
我們可以將事務(wù)理解成一個函數(shù),它接受一個外界的 SQL 輸入和一個一致的數(shù)據(jù)庫悯衬,它一定會返回一個一致的數(shù)據(jù)庫弹沽。
而第二層意思其實(shí)是指邏輯上的對于開發(fā)者的要求,我們要在代碼中寫出正確的事務(wù)邏輯筋粗,比如銀行轉(zhuǎn)賬贷币,事務(wù)中的邏輯不可能只扣錢或者只加錢,這是應(yīng)用層面上對于數(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 中的一致性對事務(wù)的要求不止包含對數(shù)據(jù)完整性以及合法性的檢查役纹,還包含應(yīng)用層面邏輯的正確。
CAP 定理中的數(shù)據(jù)一致性暇唾,其實(shí)是說分布式系統(tǒng)中的各個節(jié)點(diǎn)中對于同一數(shù)據(jù)的拷貝有著相同的值促脉;而 ACID 中的一致性是指數(shù)據(jù)庫的規(guī)則,如果 schema 中規(guī)定了一個值必須是唯一的策州,那么一致的系統(tǒng)必須確保在所有的操作中瘸味,該值都是唯一的,由此來看 CAP 和 ACID 對于一致性的定義有著根本性的區(qū)別够挂。
總結(jié)
事務(wù)的 ACID 四大基本特性是保證數(shù)據(jù)庫能夠運(yùn)行的基石旁仿,但是完全保證數(shù)據(jù)庫的 ACID,尤其是隔離性會對性能有比較大影響孽糖,在實(shí)際的使用中我們也會根據(jù)業(yè)務(wù)的需求對隔離性進(jìn)行調(diào)整枯冈,除了隔離性,數(shù)據(jù)庫的原子性和持久性相信都是比較好理解的特性办悟,前者保證數(shù)據(jù)庫的事務(wù)要么全部執(zhí)行尘奏、要么全部不執(zhí)行,后者保證了對數(shù)據(jù)庫的寫入都是持久存儲的病蛉、非易失的炫加,而一致性不僅是數(shù)據(jù)庫對本身數(shù)據(jù)的完整性的要求瑰煎,同時也對開發(fā)者提出了要求 - 寫出邏輯正確并且合理的事務(wù)。
最后俗孝,也是最重要的酒甸,當(dāng)別人在將一致性的時候,一定要搞清楚他的上下文赋铝,如果對文章的內(nèi)容有疑問烘挫,可以在評論中留言。
淺談數(shù)據(jù)庫并發(fā)控制 - 鎖和 MVCC
轉(zhuǎn)自https://draveness.me/database-concurrency-control
在學(xué)習(xí)幾年編程之后柬甥,你會發(fā)現(xiàn)所有的問題都沒有簡單、快捷的解決方案其垄,很多問題都需要權(quán)衡和妥協(xié)苛蒲,而本文介紹的就是數(shù)據(jù)庫在并發(fā)性能和可串行化之間做的權(quán)衡和妥協(xié) - 并發(fā)控制機(jī)制。
如果數(shù)據(jù)庫中的所有事務(wù)都是串行執(zhí)行的绿满,那么它非常容易成為整個應(yīng)用的性能瓶頸臂外,雖然說沒法水平擴(kuò)展的節(jié)點(diǎn)在最后都會成為瓶頸,但是串行執(zhí)行事務(wù)的數(shù)據(jù)庫會加速這一過程喇颁;而并發(fā)(Concurrency)使一切事情的發(fā)生都有了可能漏健,它能夠解決一定的性能問題,但是它會帶來更多詭異的錯誤橘霎。
引入了并發(fā)事務(wù)之后蔫浆,如果不對事務(wù)的執(zhí)行進(jìn)行控制就會出現(xiàn)各種各樣的問題,你可能沒有享受到并發(fā)帶來的性能提升就已經(jīng)被各種奇怪的問題折磨的欲仙欲死了姐叁。
概述
如何控制并發(fā)是數(shù)據(jù)庫領(lǐng)域中非常重要的問題之一瓦盛,不過到今天為止事務(wù)并發(fā)的控制已經(jīng)有了很多成熟的解決方案,而這些方案的原理就是這篇文章想要介紹的內(nèi)容外潜,文章中會介紹最為常見的三種并發(fā)控制機(jī)制:
分別是悲觀并發(fā)控制原环、樂觀并發(fā)控制和多版本并發(fā)控制,其中悲觀并發(fā)控制其實(shí)是最常見的并發(fā)控制機(jī)制处窥,也就是鎖嘱吗;而樂觀并發(fā)控制其實(shí)也有另一個名字:樂觀鎖,樂觀鎖其實(shí)并不是一種真實(shí)存在的鎖滔驾,我們會在文章后面的部分中具體介紹谒麦;最后就是多版本并發(fā)控制(MVCC)了,與前兩者對立的命名不同哆致,MVCC 可以與前兩者中的任意一種機(jī)制結(jié)合使用弄匕,以提高數(shù)據(jù)庫的讀性能。
既然這篇文章介紹了不同的并發(fā)控制機(jī)制沽瞭,那么一定會涉及到不同事務(wù)的并發(fā)迁匠,我們會通過示意圖的方式分析各種機(jī)制是如何工作的。
悲觀并發(fā)控制
控制不同的事務(wù)對同一份數(shù)據(jù)的獲取是保證數(shù)據(jù)庫的一致性的最根本方法,如果我們能夠讓事務(wù)在同一時間對同一資源有著獨(dú)占的能力城丧,那么就可以保證操作同一資源的不同事務(wù)不會相互影響延曙。
最簡單的、應(yīng)用最廣的方法就是使用鎖來解決亡哄,當(dāng)事務(wù)需要對資源進(jìn)行操作時需要先獲得資源對應(yīng)的鎖枝缔,保證其他事務(wù)不會訪問該資源后,在對資源進(jìn)行各種操作蚊惯;在悲觀并發(fā)控制中愿卸,數(shù)據(jù)庫程序?qū)τ跀?shù)據(jù)被修改持悲觀的態(tài)度,在數(shù)據(jù)處理的過程中都會被鎖定截型,以此來解決競爭的問題趴荸。
讀寫鎖
為了最大化數(shù)據(jù)庫事務(wù)的并發(fā)能力,數(shù)據(jù)庫中的鎖被設(shè)計(jì)為兩種模式宦焦,分別是共享鎖和互斥鎖发钝。當(dāng)一個事務(wù)獲得共享鎖之后,它只可以進(jìn)行讀操作波闹,所以共享鎖也叫讀鎖酝豪;而當(dāng)一個事務(wù)獲得一行數(shù)據(jù)的互斥鎖時,就可以對該行數(shù)據(jù)進(jìn)行讀和寫操作精堕,所以互斥鎖也叫寫鎖孵淘。
共享鎖和互斥鎖除了限制事務(wù)能夠執(zhí)行的讀寫操作之外,它們之間還有『共享』和『互斥』的關(guān)系歹篓,也就是多個事務(wù)可以同時獲得某一行數(shù)據(jù)的共享鎖夺英,但是互斥鎖與共享鎖和其他的互斥鎖并不兼容,我們可以很自然地理解這么設(shè)計(jì)的原因:多個事務(wù)同時寫入同一數(shù)據(jù)難免會發(fā)生各種詭異的問題滋捶。
如果當(dāng)前事務(wù)沒有辦法獲取該行數(shù)據(jù)對應(yīng)的鎖時就會陷入等待的狀態(tài)痛悯,直到其他事務(wù)將當(dāng)前數(shù)據(jù)對應(yīng)的鎖釋放才可以獲得鎖并執(zhí)行相應(yīng)的操作。
兩階段鎖協(xié)議
兩階段鎖協(xié)議(2PL)是一種能夠保證事務(wù)可串行化的協(xié)議重窟,它將事務(wù)的獲取鎖和釋放鎖劃分成了增長(Growing)和縮減(Shrinking)兩個不同的階段载萌。
在增長階段,一個事務(wù)可以獲得鎖但是不能釋放鎖巡扇;而在縮減階段事務(wù)只可以釋放鎖扭仁,并不能獲得新的鎖,如果只看 2PL 的定義厅翔,那么到這里就已經(jīng)介紹完了乖坠,但是它還有兩個變種:
Strict 2PL:事務(wù)持有的互斥鎖必須在提交后再釋放;
Rigorous 2PL:事務(wù)持有的所有鎖必須在提交后釋放刀闷;
雖然鎖的使用能夠?yàn)槲覀兘鉀Q不同事務(wù)之間由于并發(fā)執(zhí)行造成的問題熊泵,但是兩階段鎖的使用卻引入了另一個嚴(yán)重的問題仰迁,死鎖;不同的事務(wù)等待對方已經(jīng)鎖定的資源就會造成死鎖顽分,我們在這里舉一個簡單的例子:
兩個事務(wù)在剛開始時分別獲取了 draven 和 beacon 資源面的鎖徐许,然后再請求對方已經(jīng)獲得的鎖時就會發(fā)生死鎖,雙方都沒有辦法等到鎖的釋放卒蘸,如果沒有死鎖的處理機(jī)制就會無限等待下去雌隅,兩個事務(wù)都沒有辦法完成。
死鎖的處理
死鎖在多線程編程中是經(jīng)常遇到的事情缸沃,一旦涉及多個線程對資源進(jìn)行爭奪就需要考慮當(dāng)前的幾個線程或者事務(wù)是否會造成死鎖恰起;解決死鎖大體來看有兩種辦法,一種是從源頭杜絕死鎖的產(chǎn)生和出現(xiàn)趾牧,另一種是允許系統(tǒng)進(jìn)入死鎖的狀態(tài)检盼,但是在系統(tǒng)出現(xiàn)死鎖時能夠及時發(fā)現(xiàn)并且進(jìn)行恢復(fù)。
預(yù)防死鎖
有兩種方式可以幫助我們預(yù)防死鎖的出現(xiàn)武氓,一種是保證事務(wù)之間的等待不會出現(xiàn)環(huán),也就是事務(wù)之間的等待圖應(yīng)該是一張有向無環(huán)圖仇箱,沒有循環(huán)等待的情況或者保證一個事務(wù)中想要獲得的所有資源都在事務(wù)開始時以原子的方式被鎖定县恕,所有的資源要么被鎖定要么都不被鎖定。
但是這種方式有兩個問題剂桥,在事務(wù)一開始時很難判斷哪些資源是需要鎖定的忠烛,同時因?yàn)橐恍┖芡聿艜玫降臄?shù)據(jù)被提前鎖定,數(shù)據(jù)的利用率與事務(wù)的并發(fā)率也非常的低权逗。一種解決的辦法就是按照一定的順序?yàn)樗械臄?shù)據(jù)行加鎖美尸,同時與 2PL 協(xié)議結(jié)合,在加鎖階段保證所有的數(shù)據(jù)行都是從小到大依次進(jìn)行加鎖的斟薇,不過這種方式依然需要事務(wù)提前知道將要加鎖的數(shù)據(jù)集师坎。
另一種預(yù)防死鎖的方法就是使用搶占加事務(wù)回滾的方式預(yù)防死鎖,當(dāng)事務(wù)開始執(zhí)行時會先獲得一個時間戳堪滨,數(shù)據(jù)庫程序會根據(jù)事務(wù)的時間戳決定事務(wù)應(yīng)該等待還是回滾胯陋,在這時也有兩種機(jī)制供我們選擇,一種是 wait-die 機(jī)制:
當(dāng)執(zhí)行事務(wù)的時間戳小于另一事務(wù)時袱箱,即事務(wù) A 先于 B 開始遏乔,那么它就會等待另一個事務(wù)釋放對應(yīng)資源的鎖,否則就會保持當(dāng)前的時間戳并回滾发笔。
另一種機(jī)制叫做 wound-wait盟萨,這是一種搶占的解決方案,它和 wait-die 機(jī)制的結(jié)果完全相反了讨,當(dāng)前事務(wù)如果先于另一事務(wù)執(zhí)行并請求了另一事務(wù)的資源捻激,那么另一事務(wù)會立刻回滾制轰,將資源讓給先執(zhí)行的事務(wù),否則就會等待其他事務(wù)釋放資源:
兩種方法都會造成不必要的事務(wù)回滾铺罢,由此會帶來一定的性能損失艇挨,更簡單的解決死鎖的方式就是使用超時時間,但是超時時間的設(shè)定是需要仔細(xì)考慮的韭赘,否則會造成耗時較長的事務(wù)無法正常執(zhí)行缩滨,或者無法及時發(fā)現(xiàn)需要解決的死鎖,所以它的使用還是有一定的局限性泉瞻。
死鎖檢測和恢復(fù)
如果數(shù)據(jù)庫程序無法通過協(xié)議從原理上保證死鎖不會發(fā)生脉漏,那么就需要在死鎖發(fā)生時及時檢測到并從死鎖狀態(tài)恢復(fù)到正常狀態(tài)保證數(shù)據(jù)庫程序可以正常工作。在使用檢測和恢復(fù)的方式解決死鎖時袖牙,數(shù)據(jù)庫程序需要維護(hù)數(shù)據(jù)和事務(wù)之間的引用信息侧巨,同時也需要提供一個用于判斷當(dāng)前數(shù)據(jù)庫是否進(jìn)入死鎖狀態(tài)的算法,最后需要在死鎖發(fā)生時提供合適的策略及時恢復(fù)鞭达。
在上一節(jié)中我們其實(shí)提到死鎖的檢測可以通過一個有向的等待圖來進(jìn)行判斷司忱,如果一個事務(wù)依賴于另一個事務(wù)正在處理的數(shù)據(jù),那么當(dāng)前事務(wù)就會等待另一個事務(wù)的結(jié)束畴蹭,這也就是整個等待圖中的一條邊:
如上圖所示坦仍,如果在這個有向圖中出現(xiàn)了環(huán),就說明當(dāng)前數(shù)據(jù)庫進(jìn)入了死鎖的狀態(tài) TransB -> TransE -> TransF -> TransD -> TransB叨襟,在這時就需要死鎖恢復(fù)機(jī)制接入了繁扎。
如何從死鎖中恢復(fù)其實(shí)非常簡單,最常見的解決辦法就是選擇整個環(huán)中一個事務(wù)進(jìn)行回滾糊闽,以打破整個等待圖中的環(huán)梳玫,在整個恢復(fù)的過程中有三個事情需要考慮:
每次出現(xiàn)死鎖時其實(shí)都會有多個事務(wù)被波及,而選擇其中哪一個任務(wù)進(jìn)行回滾是必須要做的事情右犹,在選擇犧牲品(Victim)時的黃金原則就是最小化代價(jià)提澎,所以我們需要綜合考慮事務(wù)已經(jīng)計(jì)算的時間、使用的數(shù)據(jù)行以及涉及的事務(wù)等因素念链;當(dāng)我們選擇了犧牲品之后就可以開始回滾了虱朵,回滾其實(shí)有兩種選擇一種是全部回滾,另一種是部分回滾钓账,部分回滾會回滾到事務(wù)之前的一個檢查點(diǎn)上碴犬,如果沒有檢查點(diǎn)那自然沒有辦法進(jìn)行部分回滾。
在死鎖恢復(fù)的過程中梆暮,其實(shí)還可能出現(xiàn)某些任務(wù)在多次死鎖時都被選擇成為犧牲品服协,一直都不會成功執(zhí)行,造成饑餓(Starvation)啦粹,我們需要保證事務(wù)會在有窮的時間內(nèi)執(zhí)行偿荷,所以要在選擇犧牲品時將時間戳加入考慮的范圍窘游。
鎖的粒度
到目前為止我們都沒有對不同粒度的鎖進(jìn)行討論,一直以來我們都討論的都是數(shù)據(jù)行鎖跳纳,但是在有些時候我們希望將多個節(jié)點(diǎn)看做一個數(shù)據(jù)單元忍饰,使用鎖直接將這個數(shù)據(jù)單元、表甚至數(shù)據(jù)庫鎖定起來寺庄。這個目標(biāo)的實(shí)現(xiàn)需要我們在數(shù)據(jù)庫中定義不同粒度的鎖:
當(dāng)我們擁有了不同粒度的鎖之后艾蓝,如果某個事務(wù)想要鎖定整個數(shù)據(jù)庫或者整張表時只需要簡單的鎖住對應(yīng)的節(jié)點(diǎn)就會在當(dāng)前節(jié)點(diǎn)加上顯示(explicit)鎖,在所有的子節(jié)點(diǎn)上加隱式(implicit)鎖斗塘;雖然這種不同粒度的鎖能夠解決父節(jié)點(diǎn)被加鎖時赢织,子節(jié)點(diǎn)不能被加鎖的問題,但是我們沒有辦法在子節(jié)點(diǎn)被加鎖時馍盟,立刻確定父節(jié)點(diǎn)不能被加鎖于置。
在這時我們就需要引入意向鎖來解決這個問題了,當(dāng)需要給子節(jié)點(diǎn)加鎖時贞岭,先給所有的父節(jié)點(diǎn)加對應(yīng)的意向鎖八毯,意向鎖之間是完全不會互斥的,只是用來幫助父節(jié)點(diǎn)快速判斷是否可以對該節(jié)點(diǎn)進(jìn)行加鎖:
這里是一張引入了兩種意向鎖瞄桨,意向共享鎖和意向互斥鎖之后所有的鎖之間的兼容關(guān)系话速;到這里,我們通過不同粒度的鎖和意向鎖加快了數(shù)據(jù)庫的吞吐量讲婚。
樂觀并發(fā)控制
除了悲觀并發(fā)控制機(jī)制 - 鎖之外尿孔,我們其實(shí)還有其他的并發(fā)控制機(jī)制俊柔,樂觀并發(fā)控制(Optimistic Concurrency Control)筹麸。樂觀并發(fā)控制也叫樂觀鎖,但是它并不是真正的鎖雏婶,很多人都會誤以為樂觀鎖是一種真正的鎖物赶,然而它只是一種并發(fā)控制的思想。
在這一節(jié)中留晚,我們將會先介紹基于時間戳的并發(fā)控制機(jī)制酵紫,然后在這個協(xié)議的基礎(chǔ)上進(jìn)行擴(kuò)展,實(shí)現(xiàn)樂觀的并發(fā)控制機(jī)制错维。
基于時間戳的協(xié)議
鎖協(xié)議按照不同事務(wù)對同一數(shù)據(jù)項(xiàng)請求的時間依次執(zhí)行奖地,因?yàn)楹竺鎴?zhí)行的事務(wù)想要獲取的數(shù)據(jù)已將被前面的事務(wù)加鎖,只能等待鎖的釋放赋焕,所以基于鎖的協(xié)議執(zhí)行事務(wù)的順序與獲得鎖的順序有關(guān)参歹。在這里想要介紹的基于時間戳的協(xié)議能夠在事務(wù)執(zhí)行之前先決定事務(wù)的執(zhí)行順序。
每一個事務(wù)都會具有一個全局唯一的時間戳隆判,它即可以使用系統(tǒng)的時鐘時間犬庇,也可以使用計(jì)數(shù)器僧界,只要能夠保證所有的時間戳都是唯一并且是隨時間遞增的就可以。
基于時間戳的協(xié)議能夠保證事務(wù)并行執(zhí)行的順序與事務(wù)按照時間戳串行執(zhí)行的效果完全相同臭挽;每一個數(shù)據(jù)項(xiàng)都有兩個時間戳捂襟,讀時間戳和寫時間戳,分別代表了當(dāng)前成功執(zhí)行對應(yīng)操作的事務(wù)的時間戳欢峰。
該協(xié)議能夠保證所有沖突的讀寫操作都能按照時間戳的大小串行執(zhí)行葬荷,在執(zhí)行對應(yīng)的操作時不需要關(guān)注其他的事務(wù)只需要關(guān)心數(shù)據(jù)項(xiàng)對應(yīng)時間戳的值就可以了:
無論是讀操作還是寫操作都會從左到右依次比較讀寫時間戳的值,如果小于當(dāng)前值就會直接被拒絕然后回滾赤赊,數(shù)據(jù)庫系統(tǒng)會給回滾的事務(wù)添加一個新的時間戳并重新執(zhí)行這個事務(wù)闯狱。
基于驗(yàn)證的協(xié)議
樂觀并發(fā)控制其實(shí)本質(zhì)上就是基于驗(yàn)證的協(xié)議,因?yàn)樵诙鄶?shù)的應(yīng)用中只讀的事務(wù)占了絕大多數(shù)抛计,事務(wù)之間因?yàn)閷懖僮髟斐蓻_突的可能非常小哄孤,也就是說大多數(shù)的事務(wù)在不需要并發(fā)控制機(jī)制也能運(yùn)行的非常好,也可以保證數(shù)據(jù)庫的一致性吹截;而并發(fā)控制機(jī)制其實(shí)向整個數(shù)據(jù)庫系統(tǒng)添加了很多的開銷瘦陈,我們其實(shí)可以通過別的策略降低這部分開銷。
而驗(yàn)證協(xié)議就是我們找到的解決辦法波俄,它根據(jù)事務(wù)的只讀或者更新將所有事務(wù)的執(zhí)行分為兩到三個階段:
在讀階段晨逝,數(shù)據(jù)庫會執(zhí)行事務(wù)中的全部讀操作和寫操作,并將所有寫后的值存入臨時變量中懦铺,并不會真正更新數(shù)據(jù)庫中的內(nèi)容捉貌;在這時候會進(jìn)入下一個階段,數(shù)據(jù)庫程序會檢查當(dāng)前的改動是否合法冬念,也就是是否有其他事務(wù)在 RAED PHASE 期間更新了數(shù)據(jù)趁窃,如果通過測試那么直接就進(jìn)入 WRITE PHASE 將所有存在臨時變量中的改動全部寫入數(shù)據(jù)庫,沒有通過測試的事務(wù)會直接被終止急前。
為了保證樂觀并發(fā)控制能夠正常運(yùn)行醒陆,我們需要知道一個事務(wù)不同階段的發(fā)生時間,包括事務(wù)開始時間裆针、驗(yàn)證階段的開始時間以及寫階段的結(jié)束時間刨摩;通過這三個時間戳,我們可以保證任意沖突的事務(wù)不會同時寫入數(shù)據(jù)庫世吨,一旦由一個事務(wù)完成了驗(yàn)證階段就會立即寫入澡刹,其他讀取了相同數(shù)據(jù)的事務(wù)就會回滾重新執(zhí)行。
作為樂觀的并發(fā)控制機(jī)制耘婚,它會假定所有的事務(wù)在最終都會通過驗(yàn)證階段并且執(zhí)行成功罢浇,而鎖機(jī)制和基于時間戳排序的協(xié)議是悲觀的,因?yàn)樗鼈儠诎l(fā)生沖突時強(qiáng)制事務(wù)進(jìn)行等待或者回滾边篮,哪怕有不需要鎖也能夠保證事務(wù)之間不會沖突的可能己莺。
多版本并發(fā)控制
到目前為止我們介紹的并發(fā)控制機(jī)制其實(shí)都是通過延遲或者終止相應(yīng)的事務(wù)來解決事務(wù)之間的競爭條件(Race condition)來保證事務(wù)的可串行化奏甫;雖然前面的兩種并發(fā)控制機(jī)制確實(shí)能夠從根本上解決并發(fā)事務(wù)的可串行化的問題,但是在實(shí)際環(huán)境中數(shù)據(jù)庫的事務(wù)大都是只讀的凌受,讀請求是寫請求的很多倍阵子,如果寫請求和讀請求之前沒有并發(fā)控制機(jī)制,那么最壞的情況也是讀請求讀到了已經(jīng)寫入的數(shù)據(jù)胜蛉,這對很多應(yīng)用完全是可以接受的挠进。
在這種大前提下,數(shù)據(jù)庫系統(tǒng)引入了另一種并發(fā)控制機(jī)制 - 多版本并發(fā)控制(Multiversion Concurrency Control)誊册,每一個寫操作都會創(chuàng)建一個新版本的數(shù)據(jù)领突,讀操作會從有限多個版本的數(shù)據(jù)中挑選一個最合適的結(jié)果直接返回;在這時案怯,讀寫操作之間的沖突就不再需要被關(guān)注君旦,而管理和快速挑選數(shù)據(jù)的版本就成了 MVCC 需要解決的主要問題。
MVCC 并不是一個與樂觀和悲觀并發(fā)控制對立的東西嘲碱,它能夠與兩者很好的結(jié)合以增加事務(wù)的并發(fā)量金砍,在目前最流行的 SQL 數(shù)據(jù)庫 MySQL 和 PostgreSQL 中都對 MVCC 進(jìn)行了實(shí)現(xiàn);但是由于它們分別實(shí)現(xiàn)了悲觀鎖和樂觀鎖麦锯,所以 MVCC 實(shí)現(xiàn)的方式也不同恕稠。
MySQL 與 MVCC
MySQL 中實(shí)現(xiàn)的多版本兩階段鎖協(xié)議(Multiversion 2PL)將 MVCC 和 2PL 的優(yōu)點(diǎn)結(jié)合了起來,每一個版本的數(shù)據(jù)行都具有一個唯一的時間戳扶欣,當(dāng)有讀事務(wù)請求時鹅巍,數(shù)據(jù)庫程序會直接從多個版本的數(shù)據(jù)項(xiàng)中具有最大時間戳的返回。
更新操作就稍微有些復(fù)雜了料祠,事務(wù)會先讀取最新版本的數(shù)據(jù)計(jì)算出數(shù)據(jù)更新后的結(jié)果骆捧,然后創(chuàng)建一個新版本的數(shù)據(jù),新數(shù)據(jù)的時間戳是目前數(shù)據(jù)行的最大版本 +1:
數(shù)據(jù)版本的刪除也是根據(jù)時間戳來選擇的术陶,MySQL 會將版本最低的數(shù)據(jù)定時從數(shù)據(jù)庫中清除以保證不會出現(xiàn)大量的遺留內(nèi)容凑懂。
PostgreSQL 與 MVCC
與 MySQL 中使用悲觀并發(fā)控制不同煤痕,PostgreSQL 中都是使用樂觀并發(fā)控制的梧宫,這也就導(dǎo)致了 MVCC 在于樂觀鎖結(jié)合時的實(shí)現(xiàn)上有一些不同,最終實(shí)現(xiàn)的叫做多版本時間戳排序協(xié)議(Multiversion Timestamp Ordering)摆碉,在這個協(xié)議中塘匣,所有的的事務(wù)在執(zhí)行之前都會被分配一個唯一的時間戳,每一個數(shù)據(jù)項(xiàng)都有讀寫兩個時間戳:
當(dāng) PostgreSQL 的事務(wù)發(fā)出了一個讀請求巷帝,數(shù)據(jù)庫直接將最新版本的數(shù)據(jù)返回忌卤,不會被任何操作阻塞,而寫操作在執(zhí)行時悦荒,事務(wù)的時間戳一定要大或者等于數(shù)據(jù)行的讀時間戳肃拜,否則就會被回滾。
這種 MVCC 的實(shí)現(xiàn)保證了讀事務(wù)永遠(yuǎn)都不會失敗并且不需要等待鎖的釋放咆课,對于讀請求遠(yuǎn)遠(yuǎn)多于寫請求的應(yīng)用程序棍厂,樂觀鎖加 MVCC 對數(shù)據(jù)庫的性能有著非常大的提升颗味;雖然這種協(xié)議能夠針對一些實(shí)際情況做出一些明顯的性能提升,但是也會導(dǎo)致兩個問題牺弹,一個是每一次讀操作都會更新讀時間戳造成兩次的磁盤寫入浦马,第二是事務(wù)之間的沖突是通過回滾解決的,所以如果沖突的可能性非常高或者回滾代價(jià)巨大张漂,數(shù)據(jù)庫的讀寫性能還不如使用傳統(tǒng)的鎖等待方式晶默。
1. MVCC簡介與實(shí)踐
MySQL 在InnoDB引擎下有當(dāng)前讀和快照讀兩種模式。
1 當(dāng)前讀即加鎖讀航攒,讀取記錄的最新版本號磺陡,會加鎖保證其他并發(fā)事物不能修改當(dāng)前記錄,直至釋放鎖漠畜。插入/更新/刪除操作默認(rèn)使用當(dāng)前讀仅政,顯示的為select語句加lock in share mode或for update的查詢也采用當(dāng)前讀模式。
2 快照讀:不加鎖盆驹,讀取記錄的快照版本圆丹,而非最新版本,使用MVCC機(jī)制躯喇,最大的好處是讀取不需要加鎖辫封,讀寫不沖突,用于讀操作多于寫操作的應(yīng)用廉丽,因此在不顯示加[lock in share mode]/[for update]的select語句倦微,即普通的一條select語句默認(rèn)都是使用快照讀MVCC實(shí)現(xiàn)模式。所以樓主的為了讓大家明白所做的演示操作正压,既有當(dāng)前讀也有快照讀……
1.1 什么是MVCC
MVCC是一種多版本并發(fā)控制機(jī)制欣福。
1.2 MVCC是為了解決什么問題?
大多數(shù)的MYSQL事務(wù)型存儲引擎,如,InnoDB,F(xiàn)alcon以及PBXT都不使用一種簡單的行鎖機(jī)制.事實(shí)上,他們都和MVCC–多版本并發(fā)控制來一起使用.
大家都應(yīng)該知道,鎖機(jī)制可以控制并發(fā)操作,但是其系統(tǒng)開銷較大,而MVCC可以在大多數(shù)情況下代替行級鎖,使用MVCC,能降低其系統(tǒng)開銷.
1.3 MVCC實(shí)現(xiàn)
MVCC是通過保存數(shù)據(jù)在某個時間點(diǎn)的快照來實(shí)現(xiàn)的. 不同存儲引擎的MVCC. 不同存儲引擎的MVCC實(shí)現(xiàn)是不同的,典型的有樂觀并發(fā)控制和悲觀并發(fā)控制.
2.MVCC 具體實(shí)現(xiàn)分析
下面,我們通過InnoDB的MVCC實(shí)現(xiàn)來分析MVCC使怎樣進(jìn)行并發(fā)控制的. InnoDB的MVCC,是通過在每行記錄后面保存兩個隱藏的列來實(shí)現(xiàn)的,這兩個列焦履,分別保存了這個行的創(chuàng)建時間拓劝,一個保存的是行的刪除時間。這里存儲的并不是實(shí)際的時間值,而是系統(tǒng)版本號(可以理解為事務(wù)的ID)嘉裤,沒開始一個新的事務(wù)郑临,系統(tǒng)版本號就會自動遞增,事務(wù)開始時刻的系統(tǒng)版本號會作為事務(wù)的ID.下面看一下在REPEATABLE READ隔離級別下,MVCC具體是如何操作的.
2.1簡單的小例子
create table yang( id int primary key auto_increment, name varchar(20));
假設(shè)系統(tǒng)的版本號從1開始.
INSERT
InnoDB為新插入的每一行保存當(dāng)前系統(tǒng)版本號作為版本號. 第一個事務(wù)ID為1屑宠;
<pre>start transaction; insert into yang values(NULL,'yang') ; insert into yang values(NULL,'long'); insert into yang values(NULL,'fei'); commit;
</pre>
對應(yīng)在數(shù)據(jù)中的表如下(后面兩列是隱藏列,我們通過查詢語句并看不到)
id | name | 創(chuàng)建時間(事務(wù)ID) | 刪除時間(事務(wù)ID) |
---|---|---|---|
1 | yang | 1 | undefined |
2 | long | 1 | undefined |
3 | fei | 1 | undefined |
SELECT
InnoDB會根據(jù)以下兩個條件檢查每行記錄: a.InnoDB只會查找版本早于當(dāng)前事務(wù)版本的數(shù)據(jù)行(也就是,行的系統(tǒng)版本號小于或等于事務(wù)的系統(tǒng)版本號)厢洞,這樣可以確保事務(wù)讀取的行,要么是在事務(wù)開始前已經(jīng)存在的,要么是事務(wù)自身插入或者修改過的. b.行的刪除版本要么未定義,要么大于當(dāng)前事務(wù)版本號,這可以確保事務(wù)讀取到的行躺翻,在事務(wù)開始之前未被刪除. 只有a,b同時滿足的記錄丧叽,才能返回作為查詢結(jié)果.
DELETE
InnoDB會為刪除的每一行保存當(dāng)前系統(tǒng)的版本號(事務(wù)的ID)作為刪除標(biāo)識. 看下面的具體例子分析: 第二個事務(wù),ID為2;
<pre>start transaction; select * from yang; //(1) select * from yang; //(2) commit;
</pre>
假設(shè)1
假設(shè)在執(zhí)行這個事務(wù)ID為2的過程中,剛執(zhí)行到(1),這時,有另一個事務(wù)ID為3往這個表里插入了一條數(shù)據(jù); 第三個事務(wù)ID為3;
<pre>start transaction; insert into yang values(NULL,'tian'); commit;
</pre>
這時表中的數(shù)據(jù)如下:
id | name | 創(chuàng)建時間(事務(wù)ID) | 刪除時間(事務(wù)ID) |
---|---|---|---|
1 | yang | 1 | undefined |
2 | long | 1 | undefined |
3 | fei | 1 | undefined |
4 | tian | 3 | undefined |
然后接著執(zhí)行事務(wù)2中的(2),由于id=4的數(shù)據(jù)的創(chuàng)建時間(事務(wù)ID為3),執(zhí)行當(dāng)前事務(wù)的ID為2,而InnoDB只會查找事務(wù)ID小于等于當(dāng)前事務(wù)ID的數(shù)據(jù)行,所以id=4的數(shù)據(jù)行并不會在執(zhí)行事務(wù)2中的(2)被檢索出來,在事務(wù)2中的兩條select 語句檢索出來的數(shù)據(jù)都只會下表:
id | name | 創(chuàng)建時間(事務(wù)ID) | 刪除時間(事務(wù)ID) |
---|---|---|---|
1 | yang | 1 | undefined |
2 | long | 1 | undefined |
3 | fei | 1 | undefined |
假設(shè)2
假設(shè)在執(zhí)行這個事務(wù)ID為2的過程中,剛執(zhí)行到(1),假設(shè)事務(wù)執(zhí)行完事務(wù)3后,接著又執(zhí)行了事務(wù)4; 第四個事務(wù):
<pre>start transaction; delete from yang where id=1; commit;
</pre>
此時數(shù)據(jù)庫中的表如下:
id | name | 創(chuàng)建時間(事務(wù)ID) | 刪除時間(事務(wù)ID) |
---|---|---|---|
1 | yang | 1 | 4 |
2 | long | 1 | undefined |
3 | fei | 1 | undefined |
4 | tian | 3 | undefined |
接著執(zhí)行事務(wù)ID為2的事務(wù)(2),根據(jù)SELECT 檢索條件可以知道,它會檢索創(chuàng)建時間(創(chuàng)建事務(wù)的ID)小于當(dāng)前事務(wù)ID的行和刪除時間(刪除事務(wù)的ID)大于當(dāng)前事務(wù)的行,而id=4的行上面已經(jīng)說過,而id=1的行由于刪除時間(刪除事務(wù)的ID)大于當(dāng)前事務(wù)的ID,所以事務(wù)2的(2)select * from yang也會把id=1的數(shù)據(jù)檢索出來.所以,事務(wù)2中的兩條select 語句檢索出來的數(shù)據(jù)都如下:
id | name | 創(chuàng)建時間(事務(wù)ID) | 刪除時間(事務(wù)ID) |
---|---|---|---|
1 | yang | 1 | 4 |
2 | long | 1 | undefined |
3 | fei | 1 | undefined |
UPDATE
InnoDB執(zhí)行UPDATE公你,實(shí)際上是新插入了一行記錄蠢正,并保存其創(chuàng)建時間為當(dāng)前事務(wù)的ID,同時保存當(dāng)前事務(wù)ID到要UPDATE的行的刪除時間.
假設(shè)3
假設(shè)在執(zhí)行完事務(wù)2的(1)后又執(zhí)行,其它用戶執(zhí)行了事務(wù)3,4,這時省店,又有一個用戶對這張表執(zhí)行了UPDATE操作: 第5個事務(wù):
<pre>start transaction; update yang set name='Long' where id=2; commit;
</pre>
根據(jù)update的更新原則:會生成新的一行,并在原來要修改的列的刪除時間列上添加本事務(wù)ID,得到表如下:
id | name | 創(chuàng)建時間(事務(wù)ID) | 刪除時間(事務(wù)ID) |
---|---|---|---|
1 | yang | 1 | 4 |
2 | long | 1 | 5 |
3 | fei | 1 | undefined |
4 | tian | 3 | undefined |
2 | Long | 5 | undefined |
繼續(xù)執(zhí)行事務(wù)2的(2),根據(jù)select 語句的檢索條件,得到下表:
id | name | 創(chuàng)建時間(事務(wù)ID) | 刪除時間(事務(wù)ID) |
---|---|---|---|
1 | yang | 1 | 4 |
2 | long | 1 | 5 |
3 | fei | 1 | undefined |
還是和事務(wù)2中(1)select 得到相同的結(jié)果.