重新學(xué)習(xí)MySQL數(shù)據(jù)庫6:淺談MySQL的中事務(wù)與鎖

本文轉(zhuǎn)自互聯(lián)網(wǎng)

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內(nèi)容請到我的倉庫里查看

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點(diǎn)下Star哈

文章首發(fā)于我的個(gè)人博客:

www.how2playlife.com

本文是微信公眾號【Java技術(shù)江湖】的《重新學(xué)習(xí)MySQL數(shù)據(jù)庫》其中一篇,本文部分內(nèi)容來源于網(wǎng)絡(luò),為了把本文主題講得清晰透徹,也整合了很多我認(rèn)為不錯(cuò)的技術(shù)博客內(nèi)容,引用其中了一些比較好的博客文章,如有侵權(quán),請聯(lián)系作者。

該系列博文會(huì)告訴你如何從入門到進(jìn)階,從sql基本的使用方法,從MySQL執(zhí)行引擎再到索引、事務(wù)等知識,一步步地學(xué)習(xí)MySQL相關(guān)技術(shù)的實(shí)現(xiàn)原理,更好地了解如何基于這些知識來優(yōu)化sql,減少SQL執(zhí)行時(shí)間,通過執(zhí)行計(jì)劃對SQL性能進(jìn)行分析,再到MySQL的主從復(fù)制、主備部署等內(nèi)容,以便讓你更完整地了解整個(gè)MySQL方面的技術(shù)體系,形成自己的知識框架。

如果對本系列文章有什么建議,或者是有什么疑問的話,也可以關(guān)注公眾號【Java技術(shù)江湖】聯(lián)系作者,歡迎你參與本系列博文的創(chuàng)作和修訂。

『淺入深出』MySQL 中事務(wù)的實(shí)現(xiàn)

在關(guān)系型數(shù)據(jù)庫中,事務(wù)的重要性不言而喻,只要對數(shù)據(jù)庫稍有了解的人都知道事務(wù)具有 ACID 四個(gè)基本屬性,而我們不知道的可能就是數(shù)據(jù)庫是如何實(shí)現(xiàn)這四個(gè)屬性的;在這篇文章中,我們將對事務(wù)的實(shí)現(xiàn)進(jìn)行分析,嘗試?yán)斫鈹?shù)據(jù)庫是如何實(shí)現(xiàn)事務(wù)的,當(dāng)然我們也會(huì)在文章中簡單對 MySQL 中對 ACID 的實(shí)現(xiàn)進(jìn)行簡單的介紹。

事務(wù)其實(shí)就是并發(fā)控制的基本單位;相信我們都知道,事務(wù)是一個(gè)序列操作,其中的操作要么都執(zhí)行,要么都不執(zhí)行,它是一個(gè)不可分割的工作單位;數(shù)據(jù)庫事務(wù)的 ACID 四大特性是事務(wù)的基礎(chǔ),了解了 ACID 是如何實(shí)現(xiàn)的,我們也就清除了事務(wù)的實(shí)現(xiàn),接下來我們將依次介紹數(shù)據(jù)庫是如何實(shí)現(xiàn)這四個(gè)特性的。

原子性

在學(xué)習(xí)事務(wù)時(shí),經(jīng)常有人會(huì)告訴你,事務(wù)就是一系列的操作,要么全部都執(zhí)行,要都不執(zhí)行,這其實(shí)就是對事務(wù)原子性的刻畫;雖然事務(wù)具有原子性,但是原子性并不是只與事務(wù)有關(guān)系,它的身影在很多地方都會(huì)出現(xiàn)。

由于操作并不具有原子性,并且可以再分為多個(gè)操作,當(dāng)這些操作出現(xiàn)錯(cuò)誤或拋出異常時(shí),整個(gè)操作就可能不會(huì)繼續(xù)執(zhí)行下去,而已經(jīng)進(jìn)行的操作造成的副作用就可能造成數(shù)據(jù)更新的丟失或者錯(cuò)誤。

事務(wù)其實(shí)和一個(gè)操作沒有什么太大的區(qū)別,它是一系列的數(shù)據(jù)庫操作(可以理解為 SQL)的集合,如果事務(wù)不具備原子性,那么就沒辦法保證同一個(gè)事務(wù)中的所有操作都被執(zhí)行或者未被執(zhí)行了,整個(gè)數(shù)據(jù)庫系統(tǒng)就既不可用也不可信。

回滾日志

想要保證事務(wù)的原子性,就需要在異常發(fā)生時(shí),對已經(jīng)執(zhí)行的操作進(jìn)行回滾,而在 MySQL 中,恢復(fù)機(jī)制是通過回滾日志(undo log)實(shí)現(xiàn)的,所有事務(wù)進(jìn)行的修改都會(huì)先記錄到這個(gè)回滾日志中,然后在對數(shù)據(jù)庫中的對應(yīng)行進(jìn)行寫入。

這個(gè)過程其實(shí)非常好理解,為了能夠在發(fā)生錯(cuò)誤時(shí)撤銷之前的全部操作,肯定是需要將之前的操作都記錄下來的,這樣在發(fā)生錯(cuò)誤時(shí)才可以回滾。

回滾日志除了能夠在發(fā)生錯(cuò)誤或者用戶執(zhí)行 ROLLBACK 時(shí)提供回滾相關(guān)的信息,它還能夠在整個(gè)系統(tǒng)發(fā)生崩潰、數(shù)據(jù)庫進(jìn)程直接被殺死后,當(dāng)用戶再次啟動(dòng)數(shù)據(jù)庫進(jìn)程時(shí),還能夠立刻通過查詢回滾日志將之前未完成的事務(wù)進(jìn)行回滾,這也就需要回滾日志必須先于數(shù)據(jù)持久化到磁盤上,是我們需要先寫日志后寫數(shù)據(jù)庫的主要原因。

回滾日志并不能將數(shù)據(jù)庫物理地恢復(fù)到執(zhí)行語句或者事務(wù)之前的樣子;它是邏輯日志,當(dāng)回滾日志被使用時(shí),它只會(huì)按照日志邏輯地將數(shù)據(jù)庫中的修改撤銷掉看,可以理解為,我們在事務(wù)中使用的每一條 INSERT 都對應(yīng)了一條 DELETE,每一條 UPDATE 也都對應(yīng)一條相反的 UPDATE 語句。

在這里,我們并不會(huì)介紹回滾日志的格式以及它是如何被管理的,本文重點(diǎn)關(guān)注在它到底是一個(gè)什么樣的東西,究竟解決了、如何解決了什么樣的問題,如果想要了解具體實(shí)現(xiàn)細(xì)節(jié)的讀者,相信網(wǎng)絡(luò)上關(guān)于回滾日志的文章一定不少。

事務(wù)的狀態(tài)

因?yàn)槭聞?wù)具有原子性,所以從遠(yuǎn)處看的話,事務(wù)就是密不可分的一個(gè)整體,事務(wù)的狀態(tài)也只有三種:Active、Commited 和 Failed,事務(wù)要不就在執(zhí)行中,要不然就是成功或者失敗的狀態(tài):

但是如果放大來看,我們會(huì)發(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í)行整個(gè)事務(wù);

雖然在發(fā)生錯(cuò)誤時(shí),整個(gè)數(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ù)情況下,我們都需要在整個(gè)事務(wù)提交后,再觸發(fā)類似的無法回滾的操作

以訂票為例,哪怕我們在整個(gè)事務(wù)結(jié)束之后,才向第三方發(fā)起請求,由于向第三方請求并獲取結(jié)果是一個(gè)需要較長事件的操作,如果在事務(wù)剛剛提交時(shí),數(shù)據(jù)庫或者服務(wù)器發(fā)生了崩潰,那么我們就非常有可能丟失發(fā)起請求這一過程,這就造成了非常嚴(yán)重的問題;而這一點(diǎn)就不是數(shù)據(jù)庫所能保證的,開發(fā)者需要在適當(dāng)?shù)臅r(shí)候查看請求是否被發(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)行提交或者回滾,在這時(shí) Transaction2 對同樣的數(shù)據(jù)進(jìn)行了讀操作并提交了事務(wù);也就是說 Transaction2 是依賴于 Transaction1 的,當(dāng) Transaction1 由于一些錯(cuò)誤需要回滾時(shí),因?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ù)量逐漸增多時(shí),整個(gè)恢復(fù)流程也會(huì)變得越來越復(fù)雜,如果我們想要從事務(wù)發(fā)生的錯(cuò)誤中恢復(fù),也不是一件那么容易的事情。

在上圖所示的一次事件中,Transaction2 依賴于 Transaction1,而 Transaction3 又依賴于 Transaction1,當(dāng) Transaction1 由于執(zhí)行出現(xiàn)問題發(fā)生回滾時(shí),為了保證事務(wù)的原子性,就會(huì)將 Transaction2 和 Transaction3 中的工作全部回滾,這種情況也叫做級聯(lián)回滾(Cascading Rollback),級聯(lián)回滾的發(fā)生會(huì)導(dǎo)致大量的工作需要撤回,是我們難以接受的,不過如果想要達(dá)到絕對的原子性,這件事情又是不得不去處理的,我們會(huì)在文章的后面具體介紹如何處理并行事務(wù)的原子性。

持久性

既然是數(shù)據(jù)庫,那么一定對數(shù)據(jù)的持久存儲(chǔ)有著非常強(qiáng)烈的需求,如果數(shù)據(jù)被寫入到數(shù)據(jù)庫中,那么數(shù)據(jù)一定能夠被安全存儲(chǔ)在磁盤上;而事務(wù)的持久性就體現(xiàn)在,一旦事務(wù)被提交,那么數(shù)據(jù)一定會(huì)被寫入到數(shù)據(jù)庫中并持久存儲(chǔ)起來。

當(dāng)事務(wù)已經(jīng)被提交之后,就無法再次回滾了,唯一能夠撤回已經(jīng)提交的事務(wù)的方式就是創(chuàng)建一個(gè)相反的事務(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)存中,所以它是易失的,另一個(gè)就是在磁盤上的重做日志文件,它是持久的

當(dāng)我們在一個(gè)事務(wù)中嘗試對數(shù)據(jù)進(jìn)行修改時(shí),它會(huì)先將數(shù)據(jù)從磁盤讀入內(nèi)存,并更新內(nèi)存中緩存的數(shù)據(jù),然后生成一條重做日志并寫入重做日志緩存,當(dāng)事務(wù)真正提交時(shí),MySQL 會(huì)將重做日志緩存中的內(nèi)容刷新到重做日志文件,再將內(nèi)存中的數(shù)據(jù)更新到磁盤上,圖中的第 4、5 步就是在事務(wù)提交時(shí)執(zhí)行的。

在 InnoDB 中,重做日志都是以 512 字節(jié)的塊的形式進(jìn)行存儲(chǔ)的,同時(shí)因?yàn)閴K的大小與磁盤扇區(qū)大小相同,所以重做日志的寫入可以保證原子性,不會(huì)由于機(jī)器斷電導(dǎo)致重做日志僅寫入一半并留下臟數(shù)據(jù)。

除了所有對數(shù)據(jù)庫的修改會(huì)產(chǎn)生重做日志,因?yàn)榛貪L日志也是需要持久存儲(chǔ)的,它們也會(huì)創(chuàng)建對應(yīng)的重做日志,在發(fā)生錯(cuò)誤后,數(shù)據(jù)庫重啟時(shí)會(huì)從重做日志中找出未被更新到數(shù)據(jù)庫磁盤中的日志重新執(zhí)行以滿足事務(wù)的持久性。

回滾日志和重做日志

到現(xiàn)在為止我們了解了 MySQL 中的兩種日志,回滾日志(undo log)和重做日志(redo log);在數(shù)據(jù)庫系統(tǒng)中,事務(wù)的原子性和持久性是由事務(wù)日志(transaction log)保證的,在實(shí)現(xiàn)時(shí)也就是上面提到的兩種日志,前者用于對事務(wù)的影響進(jìn)行撤銷,后者在錯(cuò)誤處理時(shí)對已經(jīng)提交的事務(wù)進(jìn)行重做,它們能保證兩點(diǎn):

  1. 發(fā)生錯(cuò)誤或者需要回滾的事務(wù)能夠成功回滾(原子性);

  2. 在事務(wù)提交后,數(shù)據(jù)沒來得及寫會(huì)磁盤就宕機(jī)時(shí),在下次重新啟動(dòng)后能夠成功恢復(fù)數(shù)據(jù)(持久性);

在數(shù)據(jù)庫中,這兩種日志經(jīng)常都是一起工作的,我們可以將它們整體看做一條事務(wù)日志,其中包含了事務(wù)的 ID、修改的行元素以及修改前后的值。

一條事務(wù)日志同時(shí)包含了修改前后的值,能夠非常簡單的進(jìn)行回滾和重做兩種操作,在這里我們也不會(huì)對重做和回滾日志展開進(jìn)行介紹,可能會(huì)在之后的文章談一談數(shù)據(jù)庫系統(tǒng)的恢復(fù)機(jī)制時(shí)提到兩種日志的使用。

隔離性

其實(shí)作者在之前的文章 『淺入淺出』MySQL 和 InnoDB 就已經(jīng)介紹過數(shù)據(jù)庫事務(wù)的隔離性,不過為了保證文章的獨(dú)立性和完整性,我們還會(huì)對事務(wù)的隔離性進(jìn)行介紹,介紹的內(nèi)容可能稍微有所不同。

事務(wù)的隔離性是數(shù)據(jù)庫處理數(shù)據(jù)的幾大基礎(chǔ)之一,如果沒有數(shù)據(jù)庫的事務(wù)之間沒有隔離性,就會(huì)發(fā)生在 并行事務(wù)的原子性 一節(jié)中提到的級聯(lián)回滾等問題,造成性能上的巨大損失。如果所有的事務(wù)的執(zhí)行順序都是線性的,那么對于事務(wù)的管理容易得多,但是允許事務(wù)的并行執(zhí)行卻能能夠提升吞吐量和資源利用率,并且可以減少每個(gè)事務(wù)的等待時(shí)間。

當(dāng)多個(gè)事務(wù)同時(shí)并發(fā)執(zhí)行時(shí),事務(wù)的隔離性可能就會(huì)被違反,雖然單個(gè)事務(wù)的執(zhí)行可能沒有任何錯(cuò)誤,但是從總體來看就會(huì)造成數(shù)據(jù)庫的一致性出現(xiàn)問題,而串行雖然能夠允許開發(fā)者忽略并行造成的影響,能夠很好地維護(hù)數(shù)據(jù)庫的一致性,但是卻會(huì)影響事務(wù)執(zhí)行的性能。

事務(wù)的隔離級別

所以說數(shù)據(jù)庫的隔離性和一致性其實(shí)是一個(gè)需要開發(fā)者去權(quán)衡的問題,為數(shù)據(jù)庫提供什么樣的隔離性層級也就決定了數(shù)據(jù)庫的性能以及可以達(dá)到什么樣的一致性;在 SQL 標(biāo)準(zhǔn)中定義了四種數(shù)據(jù)庫的事務(wù)的隔離級別:READ UNCOMMITED、READ COMMITED、REPEATABLE READ 和 SERIALIZABLE;每個(gè)事務(wù)的隔離級別其實(shí)都比上一級多解決了一個(gè)問題:

  • RAED UNCOMMITED:使用查詢語句不會(huì)加鎖,可能會(huì)讀到未提交的行(Dirty Read);

  • READ COMMITED:只對記錄加記錄鎖,而不會(huì)在記錄之間加間隙鎖,所以允許新的記錄插入到被鎖定記錄的附近,所以再多次使用查詢語句時(shí),可能得到不同的結(jié)果(Non-Repeatable Read);

  • REPEATABLE READ:多次讀取同一范圍的數(shù)據(jù)會(huì)返回第一次查詢的快照,不會(huì)返回不同的數(shù)據(jù)行,但是可能發(fā)生幻讀(Phantom Read);

  • SERIALIZABLE:InnoDB 隱式地將全部的查詢語句加上共享鎖,解決了幻讀的問題;

以上的所有的事務(wù)隔離級別都不允許臟寫入(Dirty Write),也就是當(dāng)前事務(wù)更新了另一個(gè)事務(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)的,我們只需要知道這個(gè)隔離級別解決了什么樣的問題,但是不同數(shù)據(jù)庫對于不同隔離級別的是實(shí)現(xiàn)細(xì)節(jié)在很多時(shí)候都會(huì)讓我們遇到意料之外的坑。

如果讀者不了解臟讀、不可重復(fù)讀和幻讀究竟是什么,可以閱讀之前的文章 『淺入淺出』MySQL 和 InnoDB,在這里我們僅放一張圖來展示各個(gè)隔離層級對這幾個(gè)問題的解決情況。

隔離級別的實(shí)現(xiàn)

數(shù)據(jù)庫對于隔離級別的實(shí)現(xiàn)就是使用并發(fā)控制機(jī)制對在同一時(shí)間執(zhí)行的事務(wù)進(jìn)行控制,限制不同的事務(wù)對于同一資源的訪問和更新,而最重要也最常見的并發(fā)控制機(jī)制,在這里我們將簡單介紹三種最重要的并發(fā)控制器機(jī)制的工作原理。

鎖是一種最為常見的并發(fā)控制機(jī)制,在一個(gè)事務(wù)中,我們并不會(huì)將整個(gè)數(shù)據(jù)庫都加鎖,而是只會(huì)鎖住那些需要訪問的數(shù)據(jù)項(xiàng), MySQL 和常見數(shù)據(jù)庫中的鎖都分為兩種,共享鎖(Shared)和互斥鎖(Exclusive),前者也叫讀鎖,后者叫寫鎖。

讀鎖保證了讀操作可以并發(fā)執(zhí)行,相互不會(huì)影響,而寫鎖保證了在更新數(shù)據(jù)庫數(shù)據(jù)時(shí)不會(huì)有其他的事務(wù)訪問或者更改同一條記錄造成不可預(yù)知的問題。

時(shí)間戳

除了鎖,另一種實(shí)現(xiàn)事務(wù)的隔離性的方式就是通過時(shí)間戳,使用這種方式實(shí)現(xiàn)事務(wù)的數(shù)據(jù)庫,例如 PostgreSQL 會(huì)為每一條記錄保留兩個(gè)字段;讀時(shí)間戳中報(bào)錯(cuò)了所有訪問該記錄的事務(wù)中的最大時(shí)間戳,而記錄行的寫時(shí)間戳中保存了將記錄改到當(dāng)前值的事務(wù)的時(shí)間戳。

使用時(shí)間戳實(shí)現(xiàn)事務(wù)的隔離性時(shí),往往都會(huì)使用樂觀鎖,先對數(shù)據(jù)進(jìn)行修改,在寫回時(shí)再去判斷當(dāng)前值,也就是時(shí)間戳是否改變過,如果沒有改變過,就寫入,否則,生成一個(gè)新的時(shí)間戳并再次更新數(shù)據(jù),樂觀鎖其實(shí)并不是真正的鎖機(jī)制,它只是一種思想,在這里并不會(huì)對它進(jìn)行展開介紹。

多版本和快照隔離

通過維護(hù)多個(gè)版本的數(shù)據(jù),數(shù)據(jù)庫可以允許事務(wù)在數(shù)據(jù)被其他事務(wù)更新時(shí)對舊版本的數(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í)能夠不等待互斥鎖的釋放直接獲取數(shù)據(jù)。

隔離性與原子性

在這里就需要簡單提一下在在原子性一節(jié)中遇到的級聯(lián)回滾等問題了,如果一個(gè)事務(wù)對數(shù)據(jù)進(jìn)行了寫入,這時(shí)就會(huì)獲取一個(gè)互斥鎖,其他的事務(wù)就想要獲得改行數(shù)據(jù)的讀鎖就必須等待寫鎖的釋放,自然就不會(huì)發(fā)生級聯(lián)回滾等問題了。

不過在大多數(shù)的數(shù)據(jù)庫,比如 MySQL 中都使用了 MVCC 等特性,也就是正常的讀方法是不需要獲取鎖的,在想要對讀取的數(shù)據(jù)進(jìn)行更新時(shí)需要使用 SELECT ... FOR UPDATE 嘗試獲取對應(yīng)行的互斥鎖,以保證不同事務(wù)可以正常工作。

一致性

作者認(rèn)為數(shù)據(jù)庫的一致性是一個(gè)非常讓人迷惑的概念,原因是數(shù)據(jù)庫領(lǐng)域其實(shí)包含兩個(gè)一致性,一個(gè)是 ACID 中的一致性、另一個(gè)是 CAP 定義中的一致性。

這兩個(gè)數(shù)據(jù)庫的一致性說的完全不是一個(gè)事情,很多很多人都對這兩者的概念有非常深的誤解,當(dāng)我們在討論數(shù)據(jù)庫的一致性時(shí),一定要清楚上下文的語義是什么,盡量明確的問出我們要討論的到底是 ACID 中的一致性還是 CAP 中的一致性。

ACID

數(shù)據(jù)庫對于 ACID 中的一致性的定義是這樣的:如果一個(gè)事務(wù)原子地在一個(gè)一致地?cái)?shù)據(jù)庫中獨(dú)立運(yùn)行,那么在它執(zhí)行之后,數(shù)據(jù)庫的狀態(tài)一定是一致的。對于這個(gè)概念,它的第一層意思就是對于數(shù)據(jù)完整性的約束,包括主鍵約束、引用約束以及一些約束檢查等等,在事務(wù)的執(zhí)行的前后以及過程中不會(huì)違背對數(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ù)理解成一個(gè)函數(shù),它接受一個(gè)外界的 SQL 輸入和一個(gè)一致的數(shù)據(jù)庫,它一定會(huì)返回一個(gè)一致的數(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)中的各個(gè)節(jié)點(diǎn)中對于同一數(shù)據(jù)的拷貝有著相同的值;而 ACID 中的一致性是指數(shù)據(jù)庫的規(guī)則,如果 schema 中規(guī)定了一個(gè)值必須是唯一的,那么一致的系統(tǒng)必須確保在所有的操作中,該值都是唯一的,由此來看 CAP 和 ACID 對于一致性的定義有著根本性的區(qū)別。

總結(jié)

事務(wù)的 ACID 四大基本特性是保證數(shù)據(jù)庫能夠運(yùn)行的基石,但是完全保證數(shù)據(jù)庫的 ACID,尤其是隔離性會(huì)對性能有比較大影響,在實(shí)際的使用中我們也會(huì)根據(jù)業(yè)務(wù)的需求對隔離性進(jìn)行調(diào)整,除了隔離性,數(shù)據(jù)庫的原子性和持久性相信都是比較好理解的特性,前者保證數(shù)據(jù)庫的事務(wù)要么全部執(zhí)行、要么全部不執(zhí)行,后者保證了對數(shù)據(jù)庫的寫入都是持久存儲(chǔ)的、非易失的,而一致性不僅是數(shù)據(jù)庫對本身數(shù)據(jù)的完整性的要求,同時(shí)也對開發(fā)者提出了要求 - 寫出邏輯正確并且合理的事務(wù)。

最后,也是最重要的,當(dāng)別人在將一致性的時(shí)候,一定要搞清楚他的上下文,如果對文章的內(nèi)容有疑問,可以在評論中留言。

淺談數(shù)據(jù)庫并發(fā)控制 - 鎖和 MVCC

轉(zhuǎn)自https://draveness.me/database-concurrency-control

在學(xué)習(xí)幾年編程之后,你會(huì)發(fā)現(xiàn)所有的問題都沒有簡單、快捷的解決方案,很多問題都需要權(quán)衡和妥協(xié),而本文介紹的就是數(shù)據(jù)庫在并發(fā)性能和可串行化之間做的權(quán)衡和妥協(xié) - 并發(fā)控制機(jī)制。

如果數(shù)據(jù)庫中的所有事務(wù)都是串行執(zhí)行的,那么它非常容易成為整個(gè)應(yīng)用的性能瓶頸,雖然說沒法水平擴(kuò)展的節(jié)點(diǎn)在最后都會(huì)成為瓶頸,但是串行執(zhí)行事務(wù)的數(shù)據(jù)庫會(huì)加速這一過程;而并發(fā)(Concurrency)使一切事情的發(fā)生都有了可能,它能夠解決一定的性能問題,但是它會(huì)帶來更多詭異的錯(cuò)誤。

引入了并發(fā)事務(wù)之后,如果不對事務(wù)的執(zhí)行進(jìn)行控制就會(huì)出現(xiàn)各種各樣的問題,你可能沒有享受到并發(fā)帶來的性能提升就已經(jīng)被各種奇怪的問題折磨的欲仙欲死了。

概述

如何控制并發(fā)是數(shù)據(jù)庫領(lǐng)域中非常重要的問題之一,不過到今天為止事務(wù)并發(fā)的控制已經(jīng)有了很多成熟的解決方案,而這些方案的原理就是這篇文章想要介紹的內(nèi)容,文章中會(huì)介紹最為常見的三種并發(fā)控制機(jī)制:

分別是悲觀并發(fā)控制、樂觀并發(fā)控制和多版本并發(fā)控制,其中悲觀并發(fā)控制其實(shí)是最常見的并發(fā)控制機(jī)制,也就是鎖;而樂觀并發(fā)控制其實(shí)也有另一個(gè)名字:樂觀鎖,樂觀鎖其實(shí)并不是一種真實(shí)存在的鎖,我們會(huì)在文章后面的部分中具體介紹;最后就是多版本并發(fā)控制(MVCC)了,與前兩者對立的命名不同,MVCC 可以與前兩者中的任意一種機(jī)制結(jié)合使用,以提高數(shù)據(jù)庫的讀性能。

既然這篇文章介紹了不同的并發(fā)控制機(jī)制,那么一定會(huì)涉及到不同事務(wù)的并發(fā),我們會(huì)通過示意圖的方式分析各種機(jī)制是如何工作的。

悲觀并發(fā)控制

控制不同的事務(wù)對同一份數(shù)據(jù)的獲取是保證數(shù)據(jù)庫的一致性的最根本方法,如果我們能夠讓事務(wù)在同一時(shí)間對同一資源有著獨(dú)占的能力,那么就可以保證操作同一資源的不同事務(wù)不會(huì)相互影響。

最簡單的、應(yīng)用最廣的方法就是使用鎖來解決,當(dāng)事務(wù)需要對資源進(jìn)行操作時(shí)需要先獲得資源對應(yīng)的鎖,保證其他事務(wù)不會(huì)訪問該資源后,在對資源進(jìn)行各種操作;在悲觀并發(fā)控制中,數(shù)據(jù)庫程序?qū)τ跀?shù)據(jù)被修改持悲觀的態(tài)度,在數(shù)據(jù)處理的過程中都會(huì)被鎖定,以此來解決競爭的問題。

讀寫鎖

為了最大化數(shù)據(jù)庫事務(wù)的并發(fā)能力,數(shù)據(jù)庫中的鎖被設(shè)計(jì)為兩種模式,分別是共享鎖和互斥鎖。當(dāng)一個(gè)事務(wù)獲得共享鎖之后,它只可以進(jìn)行讀操作,所以共享鎖也叫讀鎖;而當(dāng)一個(gè)事務(wù)獲得一行數(shù)據(jù)的互斥鎖時(shí),就可以對該行數(shù)據(jù)進(jìn)行讀和寫操作,所以互斥鎖也叫寫鎖。

共享鎖和互斥鎖除了限制事務(wù)能夠執(zhí)行的讀寫操作之外,它們之間還有『共享』和『互斥』的關(guān)系,也就是多個(gè)事務(wù)可以同時(shí)獲得某一行數(shù)據(jù)的共享鎖,但是互斥鎖與共享鎖和其他的互斥鎖并不兼容,我們可以很自然地理解這么設(shè)計(jì)的原因:多個(gè)事務(wù)同時(shí)寫入同一數(shù)據(jù)難免會(huì)發(fā)生各種詭異的問題。

如果當(dāng)前事務(wù)沒有辦法獲取該行數(shù)據(jù)對應(yīng)的鎖時(shí)就會(huì)陷入等待的狀態(tài),直到其他事務(wù)將當(dāng)前數(shù)據(jù)對應(yīng)的鎖釋放才可以獲得鎖并執(zhí)行相應(yīng)的操作。

兩階段鎖協(xié)議

兩階段鎖協(xié)議(2PL)是一種能夠保證事務(wù)可串行化的協(xié)議,它將事務(wù)的獲取鎖和釋放鎖劃分成了增長(Growing)和縮減(Shrinking)兩個(gè)不同的階段。

在增長階段,一個(gè)事務(wù)可以獲得鎖但是不能釋放鎖;而在縮減階段事務(wù)只可以釋放鎖,并不能獲得新的鎖,如果只看 2PL 的定義,那么到這里就已經(jīng)介紹完了,但是它還有兩個(gè)變種:

  1. Strict 2PL:事務(wù)持有的互斥鎖必須在提交后再釋放;

  2. Rigorous 2PL:事務(wù)持有的所有鎖必須在提交后釋放;

雖然鎖的使用能夠?yàn)槲覀兘鉀Q不同事務(wù)之間由于并發(fā)執(zhí)行造成的問題,但是兩階段鎖的使用卻引入了另一個(gè)嚴(yán)重的問題,死鎖;不同的事務(wù)等待對方已經(jīng)鎖定的資源就會(huì)造成死鎖,我們在這里舉一個(gè)簡單的例子:

兩個(gè)事務(wù)在剛開始時(shí)分別獲取了 draven 和 beacon 資源面的鎖,然后再請求對方已經(jīng)獲得的鎖時(shí)就會(huì)發(fā)生死鎖,雙方都沒有辦法等到鎖的釋放,如果沒有死鎖的處理機(jī)制就會(huì)無限等待下去,兩個(gè)事務(wù)都沒有辦法完成。

死鎖的處理

死鎖在多線程編程中是經(jīng)常遇到的事情,一旦涉及多個(gè)線程對資源進(jìn)行爭奪就需要考慮當(dāng)前的幾個(gè)線程或者事務(wù)是否會(huì)造成死鎖;解決死鎖大體來看有兩種辦法,一種是從源頭杜絕死鎖的產(chǎn)生和出現(xiàn),另一種是允許系統(tǒng)進(jìn)入死鎖的狀態(tài),但是在系統(tǒng)出現(xiàn)死鎖時(shí)能夠及時(shí)發(fā)現(xiàn)并且進(jìn)行恢復(fù)。

預(yù)防死鎖

有兩種方式可以幫助我們預(yù)防死鎖的出現(xiàn),一種是保證事務(wù)之間的等待不會(huì)出現(xiàn)環(huán),也就是事務(wù)之間的等待圖應(yīng)該是一張有向無環(huán)圖,沒有循環(huán)等待的情況或者保證一個(gè)事務(wù)中想要獲得的所有資源都在事務(wù)開始時(shí)以原子的方式被鎖定,所有的資源要么被鎖定要么都不被鎖定。

但是這種方式有兩個(gè)問題,在事務(wù)一開始時(shí)很難判斷哪些資源是需要鎖定的,同時(shí)因?yàn)橐恍┖芡聿艜?huì)用到的數(shù)據(jù)被提前鎖定,數(shù)據(jù)的利用率與事務(wù)的并發(fā)率也非常的低。一種解決的辦法就是按照一定的順序?yàn)樗械臄?shù)據(jù)行加鎖,同時(shí)與 2PL 協(xié)議結(jié)合,在加鎖階段保證所有的數(shù)據(jù)行都是從小到大依次進(jìn)行加鎖的,不過這種方式依然需要事務(wù)提前知道將要加鎖的數(shù)據(jù)集。

另一種預(yù)防死鎖的方法就是使用搶占加事務(wù)回滾的方式預(yù)防死鎖,當(dāng)事務(wù)開始執(zhí)行時(shí)會(huì)先獲得一個(gè)時(shí)間戳,數(shù)據(jù)庫程序會(huì)根據(jù)事務(wù)的時(shí)間戳決定事務(wù)應(yīng)該等待還是回滾,在這時(shí)也有兩種機(jī)制供我們選擇,一種是 wait-die 機(jī)制:

當(dāng)執(zhí)行事務(wù)的時(shí)間戳小于另一事務(wù)時(shí),即事務(wù) A 先于 B 開始,那么它就會(huì)等待另一個(gè)事務(wù)釋放對應(yīng)資源的鎖,否則就會(huì)保持當(dāng)前的時(shí)間戳并回滾。

另一種機(jī)制叫做 wound-wait,這是一種搶占的解決方案,它和 wait-die 機(jī)制的結(jié)果完全相反,當(dāng)前事務(wù)如果先于另一事務(wù)執(zhí)行并請求了另一事務(wù)的資源,那么另一事務(wù)會(huì)立刻回滾,將資源讓給先執(zhí)行的事務(wù),否則就會(huì)等待其他事務(wù)釋放資源:

兩種方法都會(huì)造成不必要的事務(wù)回滾,由此會(huì)帶來一定的性能損失,更簡單的解決死鎖的方式就是使用超時(shí)時(shí)間,但是超時(shí)時(shí)間的設(shè)定是需要仔細(xì)考慮的,否則會(huì)造成耗時(shí)較長的事務(wù)無法正常執(zhí)行,或者無法及時(shí)發(fā)現(xiàn)需要解決的死鎖,所以它的使用還是有一定的局限性。

死鎖檢測和恢復(fù)

如果數(shù)據(jù)庫程序無法通過協(xié)議從原理上保證死鎖不會(huì)發(fā)生,那么就需要在死鎖發(fā)生時(shí)及時(shí)檢測到并從死鎖狀態(tài)恢復(fù)到正常狀態(tài)保證數(shù)據(jù)庫程序可以正常工作。在使用檢測和恢復(fù)的方式解決死鎖時(shí),數(shù)據(jù)庫程序需要維護(hù)數(shù)據(jù)和事務(wù)之間的引用信息,同時(shí)也需要提供一個(gè)用于判斷當(dāng)前數(shù)據(jù)庫是否進(jìn)入死鎖狀態(tài)的算法,最后需要在死鎖發(fā)生時(shí)提供合適的策略及時(shí)恢復(fù)。

在上一節(jié)中我們其實(shí)提到死鎖的檢測可以通過一個(gè)有向的等待圖來進(jìn)行判斷,如果一個(gè)事務(wù)依賴于另一個(gè)事務(wù)正在處理的數(shù)據(jù),那么當(dāng)前事務(wù)就會(huì)等待另一個(gè)事務(wù)的結(jié)束,這也就是整個(gè)等待圖中的一條邊:

如上圖所示,如果在這個(gè)有向圖中出現(xiàn)了環(huán),就說明當(dāng)前數(shù)據(jù)庫進(jìn)入了死鎖的狀態(tài) TransB -> TransE -> TransF -> TransD -> TransB,在這時(shí)就需要死鎖恢復(fù)機(jī)制接入了。

如何從死鎖中恢復(fù)其實(shí)非常簡單,最常見的解決辦法就是選擇整個(gè)環(huán)中一個(gè)事務(wù)進(jìn)行回滾,以打破整個(gè)等待圖中的環(huán),在整個(gè)恢復(fù)的過程中有三個(gè)事情需要考慮:

每次出現(xiàn)死鎖時(shí)其實(shí)都會(huì)有多個(gè)事務(wù)被波及,而選擇其中哪一個(gè)任務(wù)進(jìn)行回滾是必須要做的事情,在選擇犧牲品(Victim)時(shí)的黃金原則就是最小化代價(jià),所以我們需要綜合考慮事務(wù)已經(jīng)計(jì)算的時(shí)間、使用的數(shù)據(jù)行以及涉及的事務(wù)等因素;當(dāng)我們選擇了犧牲品之后就可以開始回滾了,回滾其實(shí)有兩種選擇一種是全部回滾,另一種是部分回滾,部分回滾會(huì)回滾到事務(wù)之前的一個(gè)檢查點(diǎn)上,如果沒有檢查點(diǎn)那自然沒有辦法進(jìn)行部分回滾。

在死鎖恢復(fù)的過程中,其實(shí)還可能出現(xiàn)某些任務(wù)在多次死鎖時(shí)都被選擇成為犧牲品,一直都不會(huì)成功執(zhí)行,造成饑餓(Starvation),我們需要保證事務(wù)會(huì)在有窮的時(shí)間內(nèi)執(zhí)行,所以要在選擇犧牲品時(shí)將時(shí)間戳加入考慮的范圍。

鎖的粒度

到目前為止我們都沒有對不同粒度的鎖進(jìn)行討論,一直以來我們都討論的都是數(shù)據(jù)行鎖,但是在有些時(shí)候我們希望將多個(gè)節(jié)點(diǎn)看做一個(gè)數(shù)據(jù)單元,使用鎖直接將這個(gè)數(shù)據(jù)單元、表甚至數(shù)據(jù)庫鎖定起來。這個(gè)目標(biāo)的實(shí)現(xiàn)需要我們在數(shù)據(jù)庫中定義不同粒度的鎖:

當(dāng)我們擁有了不同粒度的鎖之后,如果某個(gè)事務(wù)想要鎖定整個(gè)數(shù)據(jù)庫或者整張表時(shí)只需要簡單的鎖住對應(yīng)的節(jié)點(diǎn)就會(huì)在當(dāng)前節(jié)點(diǎn)加上顯示(explicit)鎖,在所有的子節(jié)點(diǎn)上加隱式(implicit)鎖;雖然這種不同粒度的鎖能夠解決父節(jié)點(diǎn)被加鎖時(shí),子節(jié)點(diǎn)不能被加鎖的問題,但是我們沒有辦法在子節(jié)點(diǎn)被加鎖時(shí),立刻確定父節(jié)點(diǎn)不能被加鎖。

在這時(shí)我們就需要引入意向鎖來解決這個(gè)問題了,當(dāng)需要給子節(jié)點(diǎn)加鎖時(shí),先給所有的父節(jié)點(diǎn)加對應(yīng)的意向鎖,意向鎖之間是完全不會(huì)互斥的,只是用來幫助父節(jié)點(diǎn)快速判斷是否可以對該節(jié)點(diǎn)進(jìn)行加鎖:

這里是一張引入了兩種意向鎖,意向共享鎖和意向互斥鎖之后所有的鎖之間的兼容關(guān)系;到這里,我們通過不同粒度的鎖和意向鎖加快了數(shù)據(jù)庫的吞吐量。

樂觀并發(fā)控制

除了悲觀并發(fā)控制機(jī)制 - 鎖之外,我們其實(shí)還有其他的并發(fā)控制機(jī)制,樂觀并發(fā)控制(Optimistic Concurrency Control)。樂觀并發(fā)控制也叫樂觀鎖,但是它并不是真正的鎖,很多人都會(huì)誤以為樂觀鎖是一種真正的鎖,然而它只是一種并發(fā)控制的思想。

在這一節(jié)中,我們將會(huì)先介紹基于時(shí)間戳的并發(fā)控制機(jī)制,然后在這個(gè)協(xié)議的基礎(chǔ)上進(jìn)行擴(kuò)展,實(shí)現(xiàn)樂觀的并發(fā)控制機(jī)制。

基于時(shí)間戳的協(xié)議

鎖協(xié)議按照不同事務(wù)對同一數(shù)據(jù)項(xiàng)請求的時(shí)間依次執(zhí)行,因?yàn)楹竺鎴?zhí)行的事務(wù)想要獲取的數(shù)據(jù)已將被前面的事務(wù)加鎖,只能等待鎖的釋放,所以基于鎖的協(xié)議執(zhí)行事務(wù)的順序與獲得鎖的順序有關(guān)。在這里想要介紹的基于時(shí)間戳的協(xié)議能夠在事務(wù)執(zhí)行之前先決定事務(wù)的執(zhí)行順序。

每一個(gè)事務(wù)都會(huì)具有一個(gè)全局唯一的時(shí)間戳,它即可以使用系統(tǒng)的時(shí)鐘時(shí)間,也可以使用計(jì)數(shù)器,只要能夠保證所有的時(shí)間戳都是唯一并且是隨時(shí)間遞增的就可以。

基于時(shí)間戳的協(xié)議能夠保證事務(wù)并行執(zhí)行的順序與事務(wù)按照時(shí)間戳串行執(zhí)行的效果完全相同;每一個(gè)數(shù)據(jù)項(xiàng)都有兩個(gè)時(shí)間戳,讀時(shí)間戳和寫時(shí)間戳,分別代表了當(dāng)前成功執(zhí)行對應(yīng)操作的事務(wù)的時(shí)間戳。

該協(xié)議能夠保證所有沖突的讀寫操作都能按照時(shí)間戳的大小串行執(zhí)行,在執(zhí)行對應(yīng)的操作時(shí)不需要關(guān)注其他的事務(wù)只需要關(guān)心數(shù)據(jù)項(xiàng)對應(yīng)時(shí)間戳的值就可以了:

無論是讀操作還是寫操作都會(huì)從左到右依次比較讀寫時(shí)間戳的值,如果小于當(dāng)前值就會(huì)直接被拒絕然后回滾,數(shù)據(jù)庫系統(tǒng)會(huì)給回滾的事務(wù)添加一個(gè)新的時(shí)間戳并重新執(zhí)行這個(gè)事務(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í)向整個(gè)數(shù)據(jù)庫系統(tǒng)添加了很多的開銷,我們其實(shí)可以通過別的策略降低這部分開銷。

而驗(yàn)證協(xié)議就是我們找到的解決辦法,它根據(jù)事務(wù)的只讀或者更新將所有事務(wù)的執(zhí)行分為兩到三個(gè)階段:

在讀階段,數(shù)據(jù)庫會(huì)執(zhí)行事務(wù)中的全部讀操作和寫操作,并將所有寫后的值存入臨時(shí)變量中,并不會(huì)真正更新數(shù)據(jù)庫中的內(nèi)容;在這時(shí)候會(huì)進(jìn)入下一個(gè)階段,數(shù)據(jù)庫程序會(huì)檢查當(dāng)前的改動(dòng)是否合法,也就是是否有其他事務(wù)在 RAED PHASE 期間更新了數(shù)據(jù),如果通過測試那么直接就進(jìn)入 WRITE PHASE 將所有存在臨時(shí)變量中的改動(dòng)全部寫入數(shù)據(jù)庫,沒有通過測試的事務(wù)會(huì)直接被終止。

為了保證樂觀并發(fā)控制能夠正常運(yùn)行,我們需要知道一個(gè)事務(wù)不同階段的發(fā)生時(shí)間,包括事務(wù)開始時(shí)間、驗(yàn)證階段的開始時(shí)間以及寫階段的結(jié)束時(shí)間;通過這三個(gè)時(shí)間戳,我們可以保證任意沖突的事務(wù)不會(huì)同時(shí)寫入數(shù)據(jù)庫,一旦由一個(gè)事務(wù)完成了驗(yàn)證階段就會(huì)立即寫入,其他讀取了相同數(shù)據(jù)的事務(wù)就會(huì)回滾重新執(zhí)行。

作為樂觀的并發(fā)控制機(jī)制,它會(huì)假定所有的事務(wù)在最終都會(huì)通過驗(yàn)證階段并且執(zhí)行成功,而鎖機(jī)制和基于時(shí)間戳排序的協(xié)議是悲觀的,因?yàn)樗鼈儠?huì)在發(fā)生沖突時(shí)強(qiáng)制事務(wù)進(jìn)行等待或者回滾,哪怕有不需要鎖也能夠保證事務(wù)之間不會(huì)沖突的可能。

多版本并發(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),每一個(gè)寫操作都會(huì)創(chuàng)建一個(gè)新版本的數(shù)據(jù),讀操作會(huì)從有限多個(gè)版本的數(shù)據(jù)中挑選一個(gè)最合適的結(jié)果直接返回;在這時(shí),讀寫操作之間的沖突就不再需要被關(guān)注,而管理和快速挑選數(shù)據(jù)的版本就成了 MVCC 需要解決的主要問題。

MVCC 并不是一個(gè)與樂觀和悲觀并發(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é)合了起來,每一個(gè)版本的數(shù)據(jù)行都具有一個(gè)唯一的時(shí)間戳,當(dāng)有讀事務(wù)請求時(shí),數(shù)據(jù)庫程序會(huì)直接從多個(gè)版本的數(shù)據(jù)項(xiàng)中具有最大時(shí)間戳的返回。

更新操作就稍微有些復(fù)雜了,事務(wù)會(huì)先讀取最新版本的數(shù)據(jù)計(jì)算出數(shù)據(jù)更新后的結(jié)果,然后創(chuàng)建一個(gè)新版本的數(shù)據(jù),新數(shù)據(jù)的時(shí)間戳是目前數(shù)據(jù)行的最大版本 +1:

數(shù)據(jù)版本的刪除也是根據(jù)時(shí)間戳來選擇的,MySQL 會(huì)將版本最低的數(shù)據(jù)定時(shí)從數(shù)據(jù)庫中清除以保證不會(huì)出現(xiàn)大量的遺留內(nèi)容。

PostgreSQL 與 MVCC

與 MySQL 中使用悲觀并發(fā)控制不同,PostgreSQL 中都是使用樂觀并發(fā)控制的,這也就導(dǎo)致了 MVCC 在于樂觀鎖結(jié)合時(shí)的實(shí)現(xiàn)上有一些不同,最終實(shí)現(xiàn)的叫做多版本時(shí)間戳排序協(xié)議(Multiversion Timestamp Ordering),在這個(gè)協(xié)議中,所有的的事務(wù)在執(zhí)行之前都會(huì)被分配一個(gè)唯一的時(shí)間戳,每一個(gè)數(shù)據(jù)項(xiàng)都有讀寫兩個(gè)時(shí)間戳:

當(dāng) PostgreSQL 的事務(wù)發(fā)出了一個(gè)讀請求,數(shù)據(jù)庫直接將最新版本的數(shù)據(jù)返回,不會(huì)被任何操作阻塞,而寫操作在執(zhí)行時(shí),事務(wù)的時(shí)間戳一定要大或者等于數(shù)據(jù)行的讀時(shí)間戳,否則就會(huì)被回滾。

這種 MVCC 的實(shí)現(xiàn)保證了讀事務(wù)永遠(yuǎn)都不會(huì)失敗并且不需要等待鎖的釋放,對于讀請求遠(yuǎn)遠(yuǎn)多于寫請求的應(yīng)用程序,樂觀鎖加 MVCC 對數(shù)據(jù)庫的性能有著非常大的提升;雖然這種協(xié)議能夠針對一些實(shí)際情況做出一些明顯的性能提升,但是也會(huì)導(dǎo)致兩個(gè)問題,一個(gè)是每一次讀操作都會(huì)更新讀時(shí)間戳造成兩次的磁盤寫入,第二是事務(wù)之間的沖突是通過回滾解決的,所以如果沖突的可能性非常高或者回滾代價(jià)巨大,數(shù)據(jù)庫的讀寫性能還不如使用傳統(tǒng)的鎖等待方式。

1. MVCC簡介與實(shí)踐

MySQL 在InnoDB引擎下有當(dāng)前讀和快照讀兩種模式。

1 當(dāng)前讀即加鎖讀,讀取記錄的最新版本號,會(huì)加鎖保證其他并發(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ù)型存儲(chǔ)引擎,如,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ù)在某個(gè)時(shí)間點(diǎn)的快照來實(shí)現(xiàn)的. 不同存儲(chǔ)引擎的MVCC. 不同存儲(chǔ)引擎的MVCC實(shí)現(xiàn)是不同的,典型的有樂觀并發(fā)控制和悲觀并發(fā)控制.

2.MVCC 具體實(shí)現(xiàn)分析

下面,我們通過InnoDB的MVCC實(shí)現(xiàn)來分析MVCC使怎樣進(jìn)行并發(fā)控制的. InnoDB的MVCC,是通過在每行記錄后面保存兩個(gè)隱藏的列來實(shí)現(xiàn)的,這兩個(gè)列,分別保存了這個(gè)行的創(chuàng)建時(shí)間,一個(gè)保存的是行的刪除時(shí)間。這里存儲(chǔ)的并不是實(shí)際的時(shí)間值,而是系統(tǒng)版本號(可以理解為事務(wù)的ID),沒開始一個(gè)新的事務(wù),系統(tǒng)版本號就會(huì)自動(dòng)遞增,事務(wù)開始時(shí)刻的系統(tǒng)版本號會(huì)作為事務(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)版本號作為版本號. 第一個(gè)事務(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)建時(shí)間(事務(wù)ID) 刪除時(shí)間(事務(wù)ID)
1 yang 1 undefined
2 long 1 undefined
3 fei 1 undefined

SELECT

InnoDB會(huì)根據(jù)以下兩個(gè)條件檢查每行記錄: a.InnoDB只會(huì)查找版本早于當(dāng)前事務(wù)版本的數(shù)據(jù)行(也就是,行的系統(tǒng)版本號小于或等于事務(wù)的系統(tǒng)版本號),這樣可以確保事務(wù)讀取的行,要么是在事務(wù)開始前已經(jīng)存在的,要么是事務(wù)自身插入或者修改過的. b.行的刪除版本要么未定義,要么大于當(dāng)前事務(wù)版本號,這可以確保事務(wù)讀取到的行,在事務(wù)開始之前未被刪除. 只有a,b同時(shí)滿足的記錄,才能返回作為查詢結(jié)果.

DELETE

InnoDB會(huì)為刪除的每一行保存當(dāng)前系統(tǒng)的版本號(事務(wù)的ID)作為刪除標(biāo)識. 看下面的具體例子分析: 第二個(gè)事務(wù),ID為2;

<pre>start transaction; select * from yang; //(1) select * from yang; //(2) commit;

</pre>

假設(shè)1

假設(shè)在執(zhí)行這個(gè)事務(wù)ID為2的過程中,剛執(zhí)行到(1),這時(shí),有另一個(gè)事務(wù)ID為3往這個(gè)表里插入了一條數(shù)據(jù); 第三個(gè)事務(wù)ID為3;

<pre>start transaction; insert into yang values(NULL,'tian'); commit;

</pre>

這時(shí)表中的數(shù)據(jù)如下:

id name 創(chuàng)建時(shí)間(事務(wù)ID) 刪除時(shí)間(事務(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)建時(shí)間(事務(wù)ID為3),執(zhí)行當(dāng)前事務(wù)的ID為2,而InnoDB只會(huì)查找事務(wù)ID小于等于當(dāng)前事務(wù)ID的數(shù)據(jù)行,所以id=4的數(shù)據(jù)行并不會(huì)在執(zhí)行事務(wù)2中的(2)被檢索出來,在事務(wù)2中的兩條select 語句檢索出來的數(shù)據(jù)都只會(huì)下表:

id name 創(chuàng)建時(shí)間(事務(wù)ID) 刪除時(shí)間(事務(wù)ID)
1 yang 1 undefined
2 long 1 undefined
3 fei 1 undefined

假設(shè)2

假設(shè)在執(zhí)行這個(gè)事務(wù)ID為2的過程中,剛執(zhí)行到(1),假設(shè)事務(wù)執(zhí)行完事務(wù)3后,接著又執(zhí)行了事務(wù)4; 第四個(gè)事務(wù):

<pre>start transaction; delete from yang where id=1; commit;

</pre>

此時(shí)數(shù)據(jù)庫中的表如下:

id name 創(chuàng)建時(shí)間(事務(wù)ID) 刪除時(shí)間(事務(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 檢索條件可以知道,它會(huì)檢索創(chuàng)建時(shí)間(創(chuàng)建事務(wù)的ID)小于當(dāng)前事務(wù)ID的行和刪除時(shí)間(刪除事務(wù)的ID)大于當(dāng)前事務(wù)的行,而id=4的行上面已經(jīng)說過,而id=1的行由于刪除時(shí)間(刪除事務(wù)的ID)大于當(dāng)前事務(wù)的ID,所以事務(wù)2的(2)select * from yang也會(huì)把id=1的數(shù)據(jù)檢索出來.所以,事務(wù)2中的兩條select 語句檢索出來的數(shù)據(jù)都如下:

id name 創(chuàng)建時(shí)間(事務(wù)ID) 刪除時(shí)間(事務(wù)ID)
1 yang 1 4
2 long 1 undefined
3 fei 1 undefined

UPDATE

InnoDB執(zhí)行UPDATE,實(shí)際上是新插入了一行記錄,并保存其創(chuàng)建時(shí)間為當(dāng)前事務(wù)的ID,同時(shí)保存當(dāng)前事務(wù)ID到要UPDATE的行的刪除時(shí)間.

假設(shè)3

假設(shè)在執(zhí)行完事務(wù)2的(1)后又執(zhí)行,其它用戶執(zhí)行了事務(wù)3,4,這時(shí),又有一個(gè)用戶對這張表執(zhí)行了UPDATE操作: 第5個(gè)事務(wù):

<pre>start transaction; update yang set name='Long' where id=2; commit;

</pre>

根據(jù)update的更新原則:會(huì)生成新的一行,并在原來要修改的列的刪除時(shí)間列上添加本事務(wù)ID,得到表如下:

id name 創(chuàng)建時(shí)間(事務(wù)ID) 刪除時(shí)間(事務(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)建時(shí)間(事務(wù)ID) 刪除時(shí)間(事務(wù)ID)
1 yang 1 4
2 long 1 5
3 fei 1 undefined

還是和事務(wù)2中(1)select 得到相同的結(jié)果.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,214評論 3 426
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,315評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,699評論 1 327
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,882評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,441評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,189評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,388評論 1 372
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,613評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,112評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,334評論 2 377

推薦閱讀更多精彩內(nèi)容