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

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

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

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

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

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

www.how2playlife.com

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

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

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

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

在關(guān)系型數(shù)據(jù)庫中,事務(wù)的重要性不言而喻畦浓,只要對(duì)數(shù)據(jù)庫稍有了解的人都知道事務(wù)具有 ACID 四個(gè)基本屬性摔认,而我們不知道的可能就是數(shù)據(jù)庫是如何實(shí)現(xiàn)這四個(gè)屬性的;在這篇文章中宅粥,我們將對(duì)事務(wù)的實(shí)現(xiàn)進(jìn)行分析,嘗試?yán)斫鈹?shù)據(jù)庫是如何實(shí)現(xiàn)事務(wù)的电谣,當(dāng)然我們也會(huì)在文章中簡單對(duì) MySQL 中對(duì) 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í)就是對(duì)事務(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í)蹲坷,對(duì)已經(jīng)執(zhí)行的操作進(jìn)行回滾,而在 MySQL 中邑飒,恢復(fù)機(jī)制是通過回滾日志(undo log)實(shí)現(xiàn)的循签,所有事務(wù)進(jìn)行的修改都會(huì)先記錄到這個(gè)回滾日志中,然后在對(duì)數(shù)據(jù)庫中的對(duì)應(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ù)庫中的修改撤銷掉看,可以理解為舔琅,我們?cè)谑聞?wù)中使用的每一條 INSERT 都對(duì)應(yīng)了一條 DELETE等恐,每一條 UPDATE 也都對(duì)應(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ù),但是如果我們?cè)谑聞?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ā)類似的無法回滾的操作

以訂票為例宗雇,哪怕我們?cè)谡麄€(gè)事務(wù)結(jié)束之后,才向第三方發(fā)起請(qǐng)求莹规,由于向第三方請(qǐng)求并獲取結(jié)果是一個(gè)需要較長事件的操作赔蒲,如果在事務(wù)剛剛提交時(shí),數(shù)據(jù)庫或者服務(wù)器發(fā)生了崩潰良漱,那么我們就非常有可能丟失發(fā)起請(qǐng)求這一過程舞虱,這就造成了非常嚴(yán)重的問題;而這一點(diǎn)就不是數(shù)據(jù)庫所能保證的母市,開發(fā)者需要在適當(dāng)?shù)臅r(shí)候查看請(qǐng)求是否被發(fā)起矾兜、結(jié)果是成功還是失敗。

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

到目前為止患久,所有的事務(wù)都只是串行執(zhí)行的椅寺,一直都沒有考慮過并行執(zhí)行的問題;然而在實(shí)際工作中蒋失,并行執(zhí)行的事務(wù)才是常態(tài)返帕,然而并行任務(wù)下,卻可能出現(xiàn)非常復(fù)雜的問題:

當(dāng) Transaction1 在執(zhí)行的過程中對(duì) id = 1 的用戶進(jìn)行了讀寫篙挽,但是沒有將修改的內(nèi)容進(jìn)行提交或者回滾荆萤,在這時(shí) Transaction2 對(duì)同樣的數(shù)據(jù)進(jìn)行了讀操作并提交了事務(wù);也就是說 Transaction2 是依賴于 Transaction1 的嫉髓,當(dāng) Transaction1 由于一些錯(cuò)誤需要回滾時(shí)观腊,因?yàn)橐WC事務(wù)的原子性,需要對(duì) 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 中的工作全部回滾,這種情況也叫做級(jí)聯(lián)回滾(Cascading Rollback)党晋,級(jí)聯(lián)回滾的發(fā)生會(huì)導(dǎo)致大量的工作需要撤回谭胚,是我們難以接受的,不過如果想要達(dá)到絕對(duì)的原子性未玻,這件事情又是不得不去處理的灾而,我們會(huì)在文章的后面具體介紹如何處理并行事務(wù)的原子性。

持久性

既然是數(shù)據(jù)庫深胳,那么一定對(duì)數(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ù)對(duì)原操作進(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)我們?cè)谝粋€(gè)事務(wù)中嘗試對(duì)數(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ù)。

除了所有對(duì)數(shù)據(jù)庫的修改會(huì)產(chǎn)生重做日志征绎,因?yàn)榛貪L日志也是需要持久存儲(chǔ)的蹲姐,它們也會(huì)創(chuàng)建對(duì)應(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í)也就是上面提到的兩種日志哥放,前者用于對(duì)事務(wù)的影響進(jìn)行撤銷歼指,后者在錯(cuò)誤處理時(shí)對(duì)已經(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ì)對(duì)重做和回滾日志展開進(jìn)行介紹,可能會(huì)在之后的文章談一談數(shù)據(jù)庫系統(tǒng)的恢復(fù)機(jī)制時(shí)提到兩種日志的使用挨务。

隔離性

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

事務(wù)的隔離性是數(shù)據(jù)庫處理數(shù)據(jù)的幾大基礎(chǔ)之一,如果沒有數(shù)據(jù)庫的事務(wù)之間沒有隔離性朝巫,就會(huì)發(fā)生在 并行事務(wù)的原子性 一節(jié)中提到的級(jí)聯(lián)回滾等問題鸿摇,造成性能上的巨大損失。如果所有的事務(wù)的執(zhí)行順序都是線性的劈猿,那么對(duì)于事務(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ù)的隔離級(jí)別

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

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

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

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

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

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

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

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

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

數(shù)據(jù)庫對(duì)于隔離級(jí)別的實(shí)現(xiàn)就是使用并發(fā)控制機(jī)制對(duì)在同一時(shí)間執(zhí)行的事務(wù)進(jìn)行控制维贺,限制不同的事務(wù)對(duì)于同一資源的訪問和更新它掂,而最重要也最常見的并發(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ì)使用樂觀鎖,先對(duì)數(shù)據(jù)進(jìn)行修改捷凄,在寫回時(shí)再去判斷當(dāng)前值忱详,也就是時(shí)間戳是否改變過,如果沒有改變過跺涤,就寫入匈睁,否則,生成一個(gè)新的時(shí)間戳并再次更新數(shù)據(jù)桶错,樂觀鎖其實(shí)并不是真正的鎖機(jī)制航唆,它只是一種思想,在這里并不會(huì)對(duì)它進(jìn)行展開介紹牛曹。

多版本和快照隔離

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

隔離性與原子性

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

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

一致性

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

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

ACID

數(shù)據(jù)庫對(duì)于 ACID 中的一致性的定義是這樣的:如果一個(gè)事務(wù)原子地在一個(gè)一致地?cái)?shù)據(jù)庫中獨(dú)立運(yùn)行,那么在它執(zhí)行之后觅廓,數(shù)據(jù)庫的狀態(tài)一定是一致的鼻忠。對(duì)于這個(gè)概念,它的第一層意思就是對(duì)于數(shù)據(jù)完整性的約束杈绸,包括主鍵約束帖蔓、引用約束以及一些約束檢查等等,在事務(wù)的執(zhí)行的前后以及過程中不會(huì)違背對(duì)數(shù)據(jù)完整性的約束瞳脓,所有對(duì)數(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í)是指邏輯上的對(duì)于開發(fā)者的要求,我們要在代碼中寫出正確的事務(wù)邏輯写妥,比如銀行轉(zhuǎn)賬拳球,事務(wù)中的邏輯不可能只扣錢或者只加錢,這是應(yīng)用層面上對(duì)于數(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 中的一致性對(duì)事務(wù)的要求不止包含對(duì)數(shù)據(jù)完整性以及合法性的檢查祝峻,還包含應(yīng)用層面邏輯的正確。

CAP 定理中的數(shù)據(jù)一致性扎筒,其實(shí)是說分布式系統(tǒng)中的各個(gè)節(jié)點(diǎn)中對(duì)于同一數(shù)據(jù)的拷貝有著相同的值莱找;而 ACID 中的一致性是指數(shù)據(jù)庫的規(guī)則,如果 schema 中規(guī)定了一個(gè)值必須是唯一的嗜桌,那么一致的系統(tǒng)必須確保在所有的操作中奥溺,該值都是唯一的,由此來看 CAP 和 ACID 對(duì)于一致性的定義有著根本性的區(qū)別骨宠。

總結(jié)

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

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

淺談數(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ù)之后珊蟀,如果不對(duì)事務(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)了,與前兩者對(duì)立的命名不同犯助,MVCC 可以與前兩者中的任意一種機(jī)制結(jié)合使用癣漆,以提高數(shù)據(jù)庫的讀性能。

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

悲觀并發(fā)控制

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

最簡單的坐慰、應(yīng)用最廣的方法就是使用鎖來解決较性,當(dāng)事務(wù)需要對(duì)資源進(jìn)行操作時(shí)需要先獲得資源對(duì)應(yīng)的鎖,保證其他事務(wù)不會(huì)訪問該資源后结胀,在對(duì)資源進(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í),就可以對(duì)該行數(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ù)對(duì)應(yīng)的鎖時(shí)就會(huì)陷入等待的狀態(tài)匾效,直到其他事務(wù)將當(dāng)前數(shù)據(jù)對(duì)應(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ù)等待對(duì)方已經(jīng)鎖定的資源就會(huì)造成死鎖,我們?cè)谶@里舉一個(gè)簡單的例子:

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

死鎖的處理

死鎖在多線程編程中是經(jīng)常遇到的事情,一旦涉及多個(gè)線程對(duì)資源進(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ù)釋放對(duì)應(yīng)資源的鎖,否則就會(huì)保持當(dāng)前的時(shí)間戳并回滾析砸。

另一種機(jī)制叫做 wound-wait昔字,這是一種搶占的解決方案,它和 wait-die 機(jī)制的結(jié)果完全相反首繁,當(dāng)前事務(wù)如果先于另一事務(wù)執(zhí)行并請(qǐng)求了另一事務(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í)間戳加入考慮的范圍。

鎖的粒度

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

當(dāng)我們擁有了不同粒度的鎖之后竭业,如果某個(gè)事務(wù)想要鎖定整個(gè)數(shù)據(jù)庫或者整張表時(shí)只需要簡單的鎖住對(duì)應(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)加對(duì)應(yīng)的意向鎖,意向鎖之間是完全不會(huì)互斥的为狸,只是用來幫助父節(jié)點(diǎn)快速判斷是否可以對(duì)該節(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ù)對(duì)同一數(shù)據(jù)項(xiàng)請(qǐ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í)行對(duì)應(yīng)操作的事務(wù)的時(shí)間戳源织。

該協(xié)議能夠保證所有沖突的讀寫操作都能按照時(shí)間戳的大小串行執(zhí)行,在執(zhí)行對(duì)應(yīng)的操作時(shí)不需要關(guān)注其他的事務(wù)只需要關(guān)心數(shù)據(jù)項(xiàng)對(duì)應(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ù)大都是只讀的设塔,讀請(qǐng)求是寫請(qǐng)求的很多倍,如果寫請(qǐng)求和讀請(qǐng)求之前沒有并發(fā)控制機(jī)制远舅,那么最壞的情況也是讀請(qǐng)求讀到了已經(jīng)寫入的數(shù)據(jù)闰蛔,這對(duì)很多應(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ā)控制對(duì)立的東西余佃,它能夠與兩者很好的結(jié)合以增加事務(wù)的并發(fā)量,在目前最流行的 SQL 數(shù)據(jù)庫 MySQL 和 PostgreSQL 中都對(duì) 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ù)請(qǐng)求時(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è)讀請(qǐng)求瞧毙,數(shù)據(jù)庫直接將最新版本的數(shù)據(jù)返回胧华,不會(huì)被任何操作阻塞,而寫操作在執(zhí)行時(shí)宙彪,事務(wù)的時(shí)間戳一定要大或者等于數(shù)據(jù)行的讀時(shí)間戳撑柔,否則就會(huì)被回滾。

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

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

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

1 當(dāng)前讀即加鎖讀荧琼,讀取記錄的最新版本號(hào),會(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ù)情況下代替行級(jí)鎖,使用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)版本號(hào)(可以理解為事務(wù)的ID)擅这,沒開始一個(gè)新的事務(wù)澈魄,系統(tǒng)版本號(hào)就會(huì)自動(dòng)遞增,事務(wù)開始時(shí)刻的系統(tǒng)版本號(hào)會(huì)作為事務(wù)的ID.下面看一下在REPEATABLE READ隔離級(jí)別下,MVCC具體是如何操作的.

2.1簡單的小例子

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

假設(shè)系統(tǒng)的版本號(hào)從1開始.

INSERT

InnoDB為新插入的每一行保存當(dāng)前系統(tǒng)版本號(hào)作為版本號(hào). 第一個(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>

對(duì)應(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)版本號(hào)小于或等于事務(wù)的系統(tǒng)版本號(hào))痹扇,這樣可以確保事務(wù)讀取的行,要么是在事務(wù)開始前已經(jīng)存在的溯香,要么是事務(wù)自身插入或者修改過的. b.行的刪除版本要么未定義,要么大于當(dāng)前事務(wù)版本號(hào),這可以確保事務(wù)讀取到的行鲫构,在事務(wù)開始之前未被刪除. 只有a,b同時(shí)滿足的記錄,才能返回作為查詢結(jié)果.

DELETE

InnoDB會(huì)為刪除的每一行保存當(dāng)前系統(tǒng)的版本號(hào)(事務(wù)的ID)作為刪除標(biāo)識(shí). 看下面的具體例子分析: 第二個(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è)用戶對(duì)這張表執(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)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市勉痴,隨后出現(xiàn)的幾起案子赫模,更是在濱河造成了極大的恐慌,老刑警劉巖蒸矛,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瀑罗,死亡現(xiàn)場離奇詭異胸嘴,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)斩祭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門劣像,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人停忿,你說我怎么就攤上這事∥蒙。” “怎么了席赂?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長时迫。 經(jīng)常有香客問我颅停,道長,這世上最難降的妖魔是什么掠拳? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任癞揉,我火速辦了婚禮,結(jié)果婚禮上溺欧,老公的妹妹穿的比我還像新娘喊熟。我一直安慰自己,他們只是感情好姐刁,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布芥牌。 她就那樣靜靜地躺著,像睡著了一般聂使。 火紅的嫁衣襯著肌膚如雪壁拉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天柏靶,我揣著相機(jī)與錄音弃理,去河邊找鬼。 笑死屎蜓,一個(gè)胖子當(dāng)著我的面吹牛痘昌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播炬转,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼控汉,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了返吻?” 一聲冷哼從身側(cè)響起姑子,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎测僵,沒想到半個(gè)月后街佑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谢翎,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年沐旨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了森逮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡磁携,死狀恐怖褒侧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情谊迄,我是刑警寧澤闷供,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站统诺,受9級(jí)特大地震影響歪脏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜粮呢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一婿失、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧啄寡,春花似錦豪硅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至姻乓,卻和暖如春嵌溢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蹋岩。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來泰國打工赖草, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人剪个。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓秧骑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親扣囊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子乎折,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

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