国产成人精品18p,天天干成人网,无码专区狠狠躁天天躁,美女脱精光隐私扒开免费观看

MySQL的中事務(wù)與鎖的實(shí)現

發(fā)布時(shí)間:2021-09-14 18:07 來(lái)源:億速云 閱讀:0 作者:chen 欄目: Mysql 歡迎投稿:712375056

本篇內容主要講解“的中事務(wù)與鎖的實(shí)現”,感興趣的朋友不妨來(lái)看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強。下面就讓小編來(lái)帶大家學(xué)習“MySQL的中事務(wù)與鎖的實(shí)現”吧!

MySQL 中事務(wù)的實(shí)現

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

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

原子性

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

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

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

回滾日志

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

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

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

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

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

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

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

但是如果放大來(lái)看,我們會(huì )發(fā)現事務(wù)不再是原子的,其中包括了很多中間狀態(tài),比如部分提交,事務(wù)的狀態(tài)圖也變得越來(lái)越復雜。

事務(wù)的狀態(tài)圖以及狀態(tài)的描述取自 Database System Concepts 一書(shū)中第 14 章的內容。

  • Active:事務(wù)的初始狀態(tài),表示事務(wù)正在執行;

  • Partially Commited:在最后一條語(yǔ)句執行之后;

  • Failed:發(fā)現事務(wù)無(wú)法正常執行之后;

  • Aborted:事務(wù)被回滾并且數據庫恢復到了事務(wù)進(jìn)行之前的狀態(tài)之后;

  • Commited:成功執行整個(gè)事務(wù);

雖然在發(fā)生錯誤時(shí),整個(gè)數據庫的狀態(tài)可以恢復,但是如果我們在事務(wù)中執行了諸如:向標準輸出打印日志、向外界發(fā)出郵件、沒(méi)有通過(guò)數據庫修改了磁盤(pán)上的內容甚至在事務(wù)執行期間發(fā)生了轉賬匯款,那么這些操作作為可見(jiàn)的外部輸出都是沒(méi)有辦法回滾的;這些問(wèn)題都是由應用開(kāi)發(fā)者解決和負責的,在絕大多數情況下,我們都需要在整個(gè)事務(wù)提交后,再觸發(fā)類(lèi)似的無(wú)法回滾的操作

以訂票為例,哪怕我們在整個(gè)事務(wù)結束之后,才向第三方發(fā)起請求,由于向第三方請求并獲取結果是一個(gè)需要較長(cháng)事件的操作,如果在事務(wù)剛剛提交時(shí),數據庫或者發(fā)生了崩潰,那么我們就非常有可能丟失發(fā)起請求這一過(guò)程,這就造成了非常嚴重的問(wèn)題;而這一點(diǎn)就不是數據庫所能保證的,開(kāi)發(fā)者需要在適當的時(shí)候查看請求是否被發(fā)起、結果是成功還是失敗。

并行事務(wù)的原子性

到目前為止,所有的事務(wù)都只是串行執行的,一直都沒(méi)有考慮過(guò)并行執行的問(wèn)題;然而在實(shí)際工作中,并行執行的事務(wù)才是常態(tài),然而并行任務(wù)下,卻可能出現非常復雜的問(wèn)題:

當 Transaction1 在執行的過(guò)程中對 id = 1 的用戶(hù)進(jìn)行了讀寫(xiě),但是沒(méi)有將修改的內容進(jìn)行提交或者回滾,在這時(shí) Transaction2 對同樣的數據進(jìn)行了讀操作并提交了事務(wù);也就是說(shuō) Transaction2 是依賴(lài)于 Transaction1 的,當 Transaction1 由于一些錯誤需要回滾時(shí),因為要保證事務(wù)的原子性,需要對 Transaction2 進(jìn)行回滾,但是由于我們已經(jīng)提交了 Transaction2,所以我們已經(jīng)沒(méi)有辦法進(jìn)行回滾操作,在這種問(wèn)題下我們就發(fā)生了問(wèn)題, Database System Concepts 一書(shū)中將這種現象稱(chēng)為不可恢復安排(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 .

簡(jiǎn)單理解一下,如果 Transaction2 依賴(lài)于事務(wù) Transaction1,那么事務(wù) Transaction1 必須在 Transaction2 提交之前完成提交的操作:

然而這樣還不算完,當事務(wù)的數量逐漸增多時(shí),整個(gè)恢復流程也會(huì )變得越來(lái)越復雜,如果我們想要從事務(wù)發(fā)生的錯誤中恢復,也不是一件那么容易的事情。

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

持久性

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

當事務(wù)已經(jīng)被提交之后,就無(wú)法再次回滾了,唯一能夠撤回已經(jīng)提交的事務(wù)的方式就是創(chuàng )建一個(gè)相反的事務(wù)對原操作進(jìn)行『補償』,這也是事務(wù)持久性的體現之一。

重做日志

與原子性一樣,事務(wù)的持久性也是通過(guò)日志來(lái)實(shí)現的,MySQL 使用重做日志(redo log)實(shí)現事務(wù)的持久性,重做日志由兩部分組成,一是內存中的重做日志緩沖區,因為重做日志緩沖區在內存中,所以它是易失的,另一個(gè)就是在磁盤(pán)上的重做日志文件,它是持久的

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

在 InnoDB 中,重做日志都是以 512 字節的塊的形式進(jìn)行存儲的,同時(shí)因為塊的大小與磁盤(pán)扇區大小相同,所以重做日志的寫(xiě)入可以保證原子性,不會(huì )由于機器斷電導致重做日志僅寫(xiě)入一半并留下臟數據。

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

回滾日志和重做日志

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

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

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

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

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

隔離性

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

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

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

事務(wù)的隔離級別

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

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

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

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

  • SERIALIZABLE:InnoDB 隱式地將全部的查詢(xún)語(yǔ)句加上共享鎖,解決了幻讀的問(wèn)題;

以上的所有的事務(wù)隔離級別都不允許臟寫(xiě)入(Dirty Write),也就是當前事務(wù)更新了另一個(gè)事務(wù)已經(jīng)更新但是還未提交的數據,大部分的數據庫中都使用了 READ COMMITED 作為默認的事務(wù)隔離級別,但是 MySQL 使用了 REPEATABLE READ 作為默認配置;從 RAED UNCOMMITED 到 SERIALIZABLE,隨著(zhù)事務(wù)隔離級別變得越來(lái)越嚴格,數據庫對于并發(fā)執行事務(wù)的性能也逐漸下降。

對于數據庫的使用者,從理論上說(shuō),并不需要知道事務(wù)的隔離級別是如何實(shí)現的,我們只需要知道這個(gè)隔離級別解決了什么樣的問(wèn)題,但是不同數據庫對于不同隔離級別的是實(shí)現細節在很多時(shí)候都會(huì )讓我們遇到意料之外的坑。

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

隔離級別的實(shí)現

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

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

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

時(shí)間戳

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

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

多版本和快照隔離

通過(guò)維護多個(gè)版本的數據,數據庫可以允許事務(wù)在數據被其他事務(wù)更新時(shí)對舊版本的數據進(jìn)行讀取,很多數據庫都對這一機制進(jìn)行了實(shí)現;因為所有的讀操作不再需要等待寫(xiě)鎖的釋放,所以能夠顯著(zhù)地提升讀的性能,MySQL 和 PostgreSQL 都對這一機制進(jìn)行自己的實(shí)現,也就是 MVCC,雖然各自實(shí)現的方式有所不同,MySQL 就通過(guò)文章中提到的回滾日志實(shí)現了 MVCC,保證事務(wù)并行執行時(shí)能夠不等待互斥鎖的釋放直接獲取數據。

隔離性與原子性

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

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

一致性

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

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

ACID

數據庫對于 ACID 中的一致性的定義是這樣的:如果一個(gè)事務(wù)原子地在一個(gè)一致地數據庫中獨立運行,那么在它執行之后,數據庫的狀態(tài)一定是一致的。對于這個(gè)概念,它的第一層意思就是對于數據完整性的約束,包括主鍵約束、引用約束以及一些約束檢查等等,在事務(wù)的執行的前后以及過(guò)程中不會(huì )違背對數據完整性的約束,所有對數據庫寫(xiě)入的操作都應該是合法的,并不能產(chǎn)生不合法的數據狀態(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è)函數,它接受一個(gè)外界的 SQL 輸入和一個(gè)一致的數據庫,它一定會(huì )返回一個(gè)一致的數據庫。

而第二層意思其實(shí)是指邏輯上的對于開(kāi)發(fā)者的要求,我們要在代碼中寫(xiě)出正確的事務(wù)邏輯,比如銀行轉賬,事務(wù)中的邏輯不可能只扣錢(qián)或者只加錢(qián),這是應用層面上對于數據庫一致性的要求。

Ensuring consistency for an individual transaction is the responsibility of the application programmer who codes the transaction. - Database System Concepts

數據庫 ACID 中的一致性對事務(wù)的要求不止包含對數據完整性以及合法性的檢查,還包含應用層面邏輯的正確。

CAP 定理中的數據一致性,其實(shí)是說(shuō)分布式系統中的各個(gè)節點(diǎn)中對于同一數據的拷貝有著(zhù)相同的值;而 ACID 中的一致性是指數據庫的規則,如果 schema 中規定了一個(gè)值必須是唯一的,那么一致的系統必須確保在所有的操作中,該值都是唯一的,由此來(lái)看 CAP 和 ACID 對于一致性的定義有著(zhù)根本性的區別。

總結

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

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

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

轉自 https://draveness.me/database-concurrency-control

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

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

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

概述

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

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

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

悲觀(guān)并發(fā)控制

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

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

讀寫(xiě)鎖

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

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

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

兩階段鎖協(xié)議

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

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

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

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

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

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

死鎖的處理

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

預防死鎖

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

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

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

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

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

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

死鎖檢測和恢復

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

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

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

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

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

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

鎖的粒度

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

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

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

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

樂(lè )觀(guān)并發(fā)控制

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

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

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

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

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

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

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

無(wú)論是讀操作還是寫(xiě)操作都會(huì )從左到右依次比較讀寫(xiě)時(shí)間戳的值,如果小于當前值就會(huì )直接被拒絕然后回滾,數據庫系統會(huì )給回滾的事務(wù)添加一個(gè)新的時(shí)間戳并重新執行這個(gè)事務(wù)。

基于驗證的協(xié)議

樂(lè )觀(guān)并發(fā)控制其實(shí)本質(zhì)上就是基于驗證的協(xié)議,因為在多數的應用中只讀的事務(wù)占了絕大多數,事務(wù)之間因為寫(xiě)操作造成沖突的可能非常小,也就是說(shuō)大多數的事務(wù)在不需要并發(fā)控制機制也能運行的非常好,也可以保證數據庫的一致性;而并發(fā)控制機制其實(shí)向整個(gè)數據庫系統添加了很多的開(kāi)銷(xiāo),我們其實(shí)可以通過(guò)別的策略降低這部分開(kāi)銷(xiāo)。

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

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

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

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

多版本并發(fā)控制

到目前為止我們介紹的并發(fā)控制機制其實(shí)都是通過(guò)延遲或者終止相應的事務(wù)來(lái)解決事務(wù)之間的競爭條件(Race condition)來(lái)保證事務(wù)的可串行化;雖然前面的兩種并發(fā)控制機制確實(shí)能夠從根本上解決并發(fā)事務(wù)的可串行化的問(wèn)題,但是在實(shí)際環(huán)境中數據庫的事務(wù)大都是只讀的,讀請求是寫(xiě)請求的很多倍,如果寫(xiě)請求和讀請求之前沒(méi)有并發(fā)控制機制,那么最壞的情況也是讀請求讀到了已經(jīng)寫(xiě)入的數據,這對很多應用完全是可以接受的。

在這種大前提下,數據庫系統引入了另一種并發(fā)控制機制 - 多版本并發(fā)控制(Multiversion Concurrency Control),每一個(gè)寫(xiě)操作都會(huì )創(chuàng )建一個(gè)新版本的數據,讀操作會(huì )從有限多個(gè)版本的數據中挑選一個(gè)最合適的結果直接返回;在這時(shí),讀寫(xiě)操作之間的沖突就不再需要被關(guān)注,而管理和快速挑選數據的版本就成了 MVCC 需要解決的主要問(wèn)題。

MVCC 并不是一個(gè)與樂(lè )觀(guān)和悲觀(guān)并發(fā)控制對立的東西,它能夠與兩者很好的結合以增加事務(wù)的并發(fā)量,在目前最流行的 SQL 數據庫 MySQL 和 PostgreSQL 中都對 MVCC 進(jìn)行了實(shí)現;但是由于它們分別實(shí)現了悲觀(guān)鎖和樂(lè )觀(guān)鎖,所以 MVCC 實(shí)現的方式也不同。

MySQL 與 MVCC

MySQL 中實(shí)現的多版本兩階段鎖協(xié)議(Multiversion 2PL)將 MVCC 和 2PL 的優(yōu)點(diǎn)結合了起來(lái),每一個(gè)版本的數據行都具有一個(gè)唯一的時(shí)間戳,當有讀事務(wù)請求時(shí),數據庫程序會(huì )直接從多個(gè)版本的數據項中具有最大時(shí)間戳的返回。

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

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

PostgreSQL 與 MVCC

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

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

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

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

MySQL 在InnoDB引擎下有當前讀和快照讀兩種模式。

1 當前讀即加鎖讀,讀取記錄的最新版本號,會(huì )加鎖保證其他并發(fā)事物不能修改當前記錄,直至釋放鎖。插入/更新/刪除操作默認使用當前讀,顯示的為select語(yǔ)句加lock in share mode或for update的查詢(xún)也采用當前讀模式。

2 快照讀:不加鎖,讀取記錄的快照版本,而非最新版本,使用MVCC機制,最大的好處是讀取不需要加鎖,讀寫(xiě)不沖突,用于讀操作多于寫(xiě)操作的應用,因此在不顯示加[lock in share mode]/[for update]的select語(yǔ)句,即普通的一條select語(yǔ)句默認都是使用快照讀MVCC實(shí)現模式。所以樓主的為了讓大家明白所做的演示操作,既有當前讀也有快照讀……

1.1 什么是MVCC

MVCC是一種多版本并發(fā)控制機制。

1.2 MVCC是為了解決什么問(wèn)題?

  • 大多數的MYSQL事務(wù)型存儲引擎,如,InnoDB,Falcon以及PBXT都不使用一種簡(jiǎn)單的行鎖機制.事實(shí)上,他們都和MVCC–多版本并發(fā)控制來(lái)一起使用.

  • 大家都應該知道,鎖機制可以控制并發(fā)操作,但是其系統開(kāi)銷(xiāo)較大,而MVCC可以在大多數情況下代替行級鎖,使用MVCC,能降低其系統開(kāi)銷(xiāo).

1.3 MVCC實(shí)現

MVCC是通過(guò)保存數據在某個(gè)時(shí)間點(diǎn)的快照來(lái)實(shí)現的. 不同存儲引擎的MVCC. 不同存儲引擎的MVCC實(shí)現是不同的,典型的有樂(lè )觀(guān)并發(fā)控制和悲觀(guān)并發(fā)控制.

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

下面,我們通過(guò)InnoDB的MVCC實(shí)現來(lái)分析MVCC使怎樣進(jìn)行并發(fā)控制的.  InnoDB的MVCC,是通過(guò)在每行記錄后面保存兩個(gè)隱藏的列來(lái)實(shí)現的,這兩個(gè)列,分別保存了這個(gè)行的創(chuàng )建時(shí)間,一個(gè)保存的是行的刪除時(shí)間。這里存儲的并不是實(shí)際的時(shí)間值,而是系統版本號(可以理解為事務(wù)的ID),沒(méi)開(kāi)始一個(gè)新的事務(wù),系統版本號就會(huì )自動(dòng)遞增,事務(wù)開(kāi)始時(shí)刻的系統版本號會(huì )作為事務(wù)的ID.下面看一下在REPEATABLE READ隔離級別下,MVCC具體是如何操作的.

2.1簡(jiǎn)單的小例子

create table yang(  id int primary key auto_increment,  name varchar(20));

假設系統的版本號從1開(kāi)始.

INSERT

InnoDB為新插入的每一行保存當前系統版本號作為版本號.  第一個(gè)事務(wù)ID為1;

start transaction; insert into yang values(NULL,'yang')  ; insert into yang values(NULL,'long'); insert into yang values(NULL,'fei'); commit;

對應在數據中的表如下(后面兩列是隱藏列,我們通過(guò)查詢(xún)語(yǔ)句并看不到)

SELECT

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

DELETE

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

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

假設1

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

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

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

然后接著(zhù)執行事務(wù)2中的(2),由于id=4的數據的創(chuàng )建時(shí)間(事務(wù)ID為3),執行當前事務(wù)的ID為2,而InnoDB只會(huì )查找事務(wù)ID小于等于當前事務(wù)ID的數據行,所以id=4的數據行并不會(huì )在執行事務(wù)2中的(2)被檢索出來(lái),在事務(wù)2中的兩條select 語(yǔ)句檢索出來(lái)的數據都只會(huì )下表:

假設2

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

start   transaction;  delete  from yang where id=1; commit;

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

接著(zhù)執行事務(wù)ID為2的事務(wù)(2),根據SELECT 檢索條件可以知道,它會(huì )檢索創(chuàng )建時(shí)間(創(chuàng )建事務(wù)的ID)小于當前事務(wù)ID的行和刪除時(shí)間(刪除事務(wù)的ID)大于當前事務(wù)的行,而id=4的行上面已經(jīng)說(shuō)過(guò),而id=1的行由于刪除時(shí)間(刪除事務(wù)的ID)大于當前事務(wù)的ID,所以事務(wù)2的(2)select * from yang也會(huì )把id=1的數據檢索出來(lái).所以,事務(wù)2中的兩條select 語(yǔ)句檢索出來(lái)的數據都如下:

UPDATE

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

假設3

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

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

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

繼續執行事務(wù)2的(2),根據select 語(yǔ)句的檢索條件,得到下表:

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

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng )、來(lái)自本網(wǎng)站內容采集于網(wǎng)絡(luò )互聯(lián)網(wǎng)轉載等其它媒體和分享為主,內容觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如侵犯了原作者的版權,請告知一經(jīng)查實(shí),將立刻刪除涉嫌侵權內容,聯(lián)系我們QQ:712375056,同時(shí)歡迎投稿傳遞力量。

久久天天躁狠狠躁夜夜爽| 午夜影院费试看黄| 在线天天看片免费视频观看| 国产精品亚洲专区无码WEB| AV无码中文字幕不卡一二三区| 免费国产黄线在线观看|