從更新丟失案例說InnoDB多版本并發(fā)控制(MVCC)

本文從一個Mysql丟失更新的案例入手谬俄,介紹InnoDB存儲引擎的非鎖定一致性讀取,多版本并發(fā)控制MVCC宣肚,事務(wù)隔離級別,以及InnoDB中的鎖策略悠栓。

例子

有一個銀行賬戶霉涨,里面有余額1000元,A惭适,B兩個用戶同時使用兩個ATM進(jìn)行余額查詢笙瑟,他們都看到余額為1000元,于是A用戶轉(zhuǎn)出賬戶中的900元癞志,銀行將余額更新為100元往枷,B用戶轉(zhuǎn)出賬戶中的1元,銀行將余額更新為999元。由于同時操作的原因错洁,最終該賬戶的余額有可能被更新為999元秉宿,但是賬戶卻轉(zhuǎn)出去兩筆錢,出現(xiàn)了邏輯意義上的更新丟失屯碴,模擬如下:
創(chuàng)建一個測試表描睦,user字段為自增主鍵,cash字段表示余額:

-- ----------------------------
-- Table structure for account
-- ----------------------------
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
  `user` int(11) NOT NULL AUTO_INCREMENT,
  `cash` int(11) NOT NULL DEFAULT 0,
  PRIMARY KEY (`user`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

插入一條數(shù)據(jù)

-- ----------------------------
-- Records of account
-- ----------------------------
INSERT INTO `account` VALUES ('1', '1000');

以時間順序展示sql以及執(zhí)行效果如下:

事務(wù)A 事務(wù)B 結(jié)果
1 start TRANSACTION;
select * FROM account WHERE user=1;
事務(wù)A查詢到一條記錄user=1窿锉,cash=1000
2 UPDATE account set cash=100 WHERE user=1;
select * FROM account WHERE user=1;
執(zhí)行成功酌摇;事務(wù)A再次查詢cash=100
3 start TRANSACTION;
select * FROM account WHERE user=1;
事務(wù)B查詢到一條記錄user=1,cash=1000
4 UPDATE account set cash=999 WHERE user=1; 等待
5 COMMIT; 事務(wù)A提交成功嗡载,事務(wù)B第4步執(zhí)行成功
6 COMMIT; 事務(wù)B提交成功
7 select * FROM account WHERE user=1; 此時再次進(jìn)行查詢:user=1窑多,cash=999

上面這個例子可以聯(lián)想到多線程中對共享數(shù)據(jù)的處理,如果多個線程同時修改共享數(shù)據(jù)洼滚,那么數(shù)據(jù)將會產(chǎn)生錯亂埂息,可以使用加鎖的方式對訪問和操作共享數(shù)據(jù)的代碼段(稱做臨界區(qū))進(jìn)行加鎖,使得同一時間只能有一個線程持有鎖遥巴,達(dá)到保護(hù)共享數(shù)據(jù)的目的千康。

容易想到的是,InnoDB會對多個事務(wù)同時進(jìn)行更改的數(shù)據(jù)進(jìn)行加鎖(具體鎖的類別和不同語句的設(shè)置的鎖下文中進(jìn)行說明)铲掐,以避免并發(fā)問題保證同步拾弃,事實(shí)上在上面例子中user=1這行數(shù)據(jù)確實(shí)被事物A所在的線程加鎖,使用
SELECT * FROM `performance_schema`.data_locks;

可以看到如下結(jié)果:


FssqgI.png

那么為什么在事物A加鎖期間摆霉,事務(wù)B對數(shù)據(jù)的讀取依舊沒有阻塞呢豪椿?以及在第2步中事務(wù)A更新了該行數(shù)據(jù),第3步中事務(wù)B讀取到的數(shù)據(jù)卻依舊是最初的數(shù)據(jù)cash=1000呢携栋?

一致性非鎖定讀(Consistent Nonlocking Reads)

如上所述搭盾,事務(wù)A在進(jìn)行更新行數(shù)據(jù),但是其他事物的查詢操作并沒有阻塞婉支,這是因?yàn)镮nnoDB的普通查詢采用了一致性非鎖定讀的特性鸯隅,InnoDB在某個時間點(diǎn)對數(shù)據(jù)庫查詢得到的是一個快照。查詢將查看在該時間點(diǎn)之前提交的事務(wù)所做的更改向挖,而不查看后續(xù)事務(wù)或未提交事務(wù)所做的更改蝌以。

InnoDB采用這種設(shè)計(jì)極大地提高了數(shù)據(jù)庫的并發(fā)性,使得對數(shù)據(jù)實(shí)時性并不是很高的查詢(可以接受一個不那么新鮮的數(shù)據(jù))不被阻塞何之,得到一個該行之前版本的數(shù)據(jù)跟畅,因?yàn)椴恍枰却i的釋放,所以稱其為非鎖定讀帝美。上面例子事物B之所以沒有被阻塞,是因?yàn)樗@得的是一個快照數(shù)據(jù)。

InnoDB是一個多版本的存儲引擎悼潭,多個事務(wù)可能會看到多個數(shù)據(jù)版本庇忌,這種技術(shù)就是多版本技術(shù)(Multi-Versioning),由此帶來的并發(fā)控制稱為多版本并發(fā)控制(Multiversion concurrency control)舰褪。容易看出皆疹,這種設(shè)計(jì)解決了以下問題:

  1. 事務(wù)的回滾:如果事務(wù)失敗需要回滾,那么事務(wù)可以根據(jù)快照信息構(gòu)建行的早期版本占拍,從而保證事務(wù)要么成功略就,要么失敗的原子性
  2. 讀的性能:當(dāng)事務(wù)或者更新語句鎖住行記錄時,其他事務(wù)對行的普通讀不需要等待鎖的釋放晃酒,讀的性能得到提高
  3. 讀者過多引起的寫者饑餓問題:如果不采用MVCC表牢,讀者對所讀的數(shù)據(jù)添加讀鎖,防止數(shù)據(jù)在讀的過程中被其他線程修改贝次,寫者在鎖釋放之前無法進(jìn)行更新操作崔兴,如果存在大量的讀者必定會使等待的寫者處于饑餓狀態(tài)。

那么InnoBD引擎是如何實(shí)現(xiàn)多版本的特性呢蛔翅,我們接著往下看

undo log

由于隨機(jī)訪問硬盤的速度遠(yuǎn)遠(yuǎn)低于內(nèi)存敲茄,即ms和ns的差距,因此操作系統(tǒng)會將最近使用的數(shù)據(jù)加載到內(nèi)存中山析,這樣做的考慮是出于數(shù)據(jù)一旦被訪問堰燎,在短期內(nèi)有可能被再次訪問;程序?qū)懖僮鲿r也會把數(shù)據(jù)先寫到內(nèi)存中笋轨,硬盤不會立即更新秆剪,而是把內(nèi)存中這些數(shù)據(jù)標(biāo)記為臟頁,由回寫進(jìn)程將臟頁回寫進(jìn)磁盤翩腐。

InnoDB作為高效的數(shù)據(jù)庫引擎鸟款,也是采用類似的策略,它在內(nèi)存中分配緩沖池緩存表和索引等數(shù)據(jù)茂卦。經(jīng)常使用的數(shù)據(jù)直接從緩沖池中處理何什,數(shù)據(jù)更新時首先在內(nèi)存中更新,然后異步由線程刷新到硬盤中等龙。

在上面例子的第2步中处渣,事務(wù)A確實(shí)已經(jīng)將內(nèi)存中的cash的值更新為100,它自己隨后進(jìn)行查詢也會返回更新后的值蛛砰。那么事務(wù)B的快照信息是怎么回事呢罐栈?原來,InnoDB引擎在事務(wù)更新數(shù)據(jù)時會記錄一種叫做undo log的重做日志泥畅,這些信息存儲在名為回滾段( rollback segment)的數(shù)據(jù)結(jié)構(gòu)中荠诬。InnoDB使用回滾段中的信息來執(zhí)行事務(wù)回滾所需的撤消操作。例如,如果事務(wù)進(jìn)行回滾柑贞,結(jié)合undo log方椎,對于每個插入操作,InnoDB引擎會執(zhí)行一個相反的刪除操作钧嘶,對于每個更新操作棠众,InnoDB引擎會執(zhí)行一個相反的更新操作,對于每個刪除操作有决,InnoDB引擎會執(zhí)行一個相反的插入操作闸拿,以此完成事務(wù)的回滾。

undo log的另一個作用就是實(shí)現(xiàn)MVCC书幕,事務(wù)可以根據(jù)undo log“推理”出之前的行版本信息新荤,從而實(shí)現(xiàn)非鎖定讀取,這就是例子中第3步按咒,事務(wù)B進(jìn)行查詢迟隅,得到的結(jié)果卻依舊是數(shù)據(jù)的最初版本,而不是內(nèi)存中事務(wù)A更新后值励七。

行記錄是如何找多回滾段的位置呢智袭?在內(nèi)部,InnoDB向數(shù)據(jù)庫中存儲的每一行添加三個字段掠抬。一個6字節(jié)的DB_TRX_ID字段指示插入或更新行的最后一個事務(wù)的事務(wù)標(biāo)識符吼野。一個7字節(jié)的DB_ROLL_PTR字段,稱為滾動指針两波。滾動指針指向回滾段(rollback segment)的撤銷日志(undo log record)記錄瞳步。如果更新了行,則撤消日志記錄包含在更新行之前重建行的內(nèi)容所需的信息腰奋。一個6字節(jié)的DB_ROW_ID字段包含一個行ID单起,隨著插入新的行,這個行ID會單調(diào)地增加劣坊。如果InnoDB自動生成主鍵嘀倒,則索引包含行ID值。否則局冰,DB_ROW_ID列不會出現(xiàn)在任何索引中测蘑。

事務(wù)隔離級別

說到這里,我們就能清晰的理解事務(wù)的四個隔離級別之間的差異了康二。事務(wù)隔離是數(shù)據(jù)庫處理的基礎(chǔ)之一碳胳,隔離級別是在多個事務(wù)同時進(jìn)行更改和執(zhí)行查詢時,對性能和可靠性沫勿、一致性和結(jié)果可重復(fù)性之間的平衡進(jìn)行微調(diào)的設(shè)置挨约。

InnoDB提供SQL:1992標(biāo)準(zhǔn)描述的所有四個事務(wù)隔離級別:未提交讀味混、提交讀、可重復(fù)讀和可序列化( READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, and SERIALIZABLE)诫惭。InnoDB的默認(rèn)隔離級別是可重復(fù)讀惜傲。

REPEATABLE READ

這是InnoDB的默認(rèn)隔離級別。在同一事務(wù)內(nèi)的讀取為第一次讀取建立的快照snapshot贝攒。這表示,如果在同一個事務(wù)中發(fā)出幾個普通(非鎖定)SELECT語句时甚,那么這些SELECT語句彼此之間讀取到的數(shù)據(jù)是一致的隘弊。即在上面的例子中,無論事務(wù)A提交或是未提交荒适,事務(wù)B提交前讀到的數(shù)據(jù)始終都是它在開始事物的時間點(diǎn)所看到的數(shù)據(jù)版本梨熙,所以稱為可重復(fù)讀。

READ COMMITTED

每個一致的讀取刀诬,甚至在同一個的事務(wù)中咽扇,都讀取最新快照。即在上面的例子中陕壹,事務(wù)A提交之前质欲,事務(wù)B執(zhí)行普通的select看到的余額是1000元,而在事務(wù)A提交之后糠馆,事務(wù)B執(zhí)行普通的select就可以看到事務(wù)A更新后的數(shù)據(jù)嘶伟,即余額變?yōu)?00,由于同一個事物中同一個查詢語句每次查詢結(jié)果可能不同又碌,所以稱為提交讀九昧。

READ UNCOMMITTED

這種隔離級別下,select語句可以讀取其他事務(wù)未提交的數(shù)據(jù)毕匀,即臟讀铸鹰。在上面的例子中,事務(wù)A更新余額后即使沒有提交事務(wù)皂岔,事務(wù)B的查詢也可以看到余額被更新了蹋笼,而此時事務(wù)A未必能最終執(zhí)行成功,當(dāng)其他事務(wù)回滾時凤薛,數(shù)據(jù)會產(chǎn)生不一致姓建。

SERIALIZABLE

這個級別類似于可重復(fù)讀,但是比可重復(fù)讀更嚴(yán)格缤苫,InnoDB隱式地將所有普通SELECT語句轉(zhuǎn)換為 SELECT ... LOCK IN SHARE MODE速兔,即對普通的讀取操作也進(jìn)行加鎖。在上面的例子中活玲,事務(wù)A執(zhí)行select后涣狗,事務(wù)B使用select查詢將被阻塞谍婉,直到事務(wù)A提交事務(wù)釋放鎖。

如何解決問題

到這里镀钓,我們明白了更新丟失現(xiàn)象的原因:一致性的非鎖定讀讀取到了快照數(shù)據(jù)穗熬。是時候解決問題了,在這之前丁溅,首先了解一下InnoDB提供的幾種鎖唤蔗。

共享鎖和獨(dú)占鎖

InnoDB實(shí)現(xiàn)了標(biāo)準(zhǔn)行級別鎖定,其中有兩種類型的鎖窟赏、共享鎖(shared locks妓柜,簡稱s鎖)和獨(dú)占鎖(exclusive locks,簡稱x鎖)涯穷。

  • 共享鎖允許持有鎖的事務(wù)讀取行棍掐。
  • 獨(dú)占(X)鎖允許持有鎖的事務(wù)更新或刪除一行。

例如事務(wù)T1持有行r上的共享(S)鎖拷况,那么從某些不同的事務(wù)T2中請求對行r的鎖的處理如下:

  • T2對S鎖的請求可以立即授予作煌。因此,T1和T2在r上都有S鎖赚瘦。
  • T2對X鎖的請求不能立即授予粟誓。

如果事務(wù)T1在行r上持有獨(dú)占(X)鎖,則不能立即授予來自某個不同事務(wù)T2的請求起意,以獲取r上任意類型的鎖努酸。相反,事務(wù)T2必須等待事務(wù)T1釋放在r行上的鎖杜恰。

Intention Locks意向鎖

InnoDB支持多粒度鎖获诈,允許行鎖和表鎖共存。為了使多粒度級別的鎖更實(shí)用心褐,InnoDB使用意圖鎖舔涎。意圖鎖是表級鎖,它指示在一個表中逗爹,一個事務(wù)需要稍后進(jìn)行哪些類型的鎖(共享或獨(dú)占)亡嫌。意圖鎖有兩種類型:

  • 一個意圖共享鎖(IS)表示一個事務(wù)打算在一個表中為單獨(dú)的行設(shè)置一個共享鎖。
  • 意圖獨(dú)占鎖(IX)指示事務(wù)打算在表中的單個行上設(shè)置獨(dú)占鎖掘而。

意圖鎖定協(xié)議如下

  • 事務(wù)在獲取表中一行上的共享鎖之前挟冠,必須首先獲取表上的IS鎖或更強(qiáng)的鎖。
  • 事務(wù)在獲取表中的行上的獨(dú)占鎖之前袍睡,必須首先獲取表上的IX鎖知染。
    意圖鎖除了全表請求外不會阻塞任何東西(例如, LOCK TABLES ... WRITE).)斑胜。意圖鎖定的主要目的是顯示某人正在鎖定一個行控淡,或者在表中鎖定一行嫌吠。

表級鎖類型兼容性總結(jié)如下矩陣,如果請求事務(wù)與現(xiàn)有鎖兼容掺炭,則授予鎖辫诅,但與現(xiàn)有鎖沖突則不授予鎖。事務(wù)等待直到?jīng)_突的現(xiàn)有鎖被釋放涧狮。


屏幕快照 2018-12-26 19.14.06.png

不同SQL語句設(shè)置的鎖

在最開始的示例中我們查看了事務(wù)A執(zhí)行更新后數(shù)據(jù)庫實(shí)例中存在的鎖炕矮,其中在表上有一個IX鎖,行記錄上有一個X鎖者冤,易于想到一種避免丟失更新的解決方案:在查詢的時候?qū)?shù)據(jù)行也加上X鎖吧享,不使用非鎖定讀取。普通的SELECT ... FROM使用多版本不加鎖讀取數(shù)據(jù)庫快照譬嚣,除非事務(wù)隔離級別設(shè)置為 SERIALIZABLE可序列化。

MYSQL提供了加鎖的讀取方式: SELECT ... FOR UPDATE 會為符合條件的行添加排他鎖钞它,SELECT ... LOCK IN SHARE MODE會為符合條件的行添加共享鎖拜银。因此,我們使用SELECT ... FOR UPDATE語句優(yōu)化示例執(zhí)行如下:

事務(wù)A 事務(wù)B 結(jié)果
1 start TRANSACTION;
select * FROM account WHERE user=1 for UPDATE;
事務(wù)A查詢到一條記錄user=1遭垛,cash=1000尼桶,并給該條行添加X鎖
2 UPDATE account set cash=100 WHERE user=1;
select * FROM account WHERE user=1;
執(zhí)行成功;事務(wù)A再次查詢cash=100
3 start TRANSACTION;
select * FROM account WHERE user=1 for UPDATE;
等待锯仪,事務(wù)A此時占有行的X鎖泵督,事務(wù)B同樣請求X鎖,因此被阻塞
4 COMMIT; 事務(wù)A提交成功庶喜,事務(wù)B第3步執(zhí)行成功小腊,返回cash=100
5 UPDATE account set cash=99 WHERE user=1; 事務(wù)B將余額減1,余額更新為99
6 COMMIT; 事務(wù)B提交成功
7 select * FROM account WHERE user=1; 此時再次進(jìn)行查詢:user=1久窟,cash=99

可以看到當(dāng)事務(wù)A使用 SELECT ... FOR UPDATE語句后秩冈,為符合條件的記錄添加X鎖,B事務(wù)不采用非鎖定讀取獲得快照數(shù)據(jù)斥扛,同樣使用SELECT ... FOR UPDATE語句進(jìn)行查詢入问,在事務(wù)A提交事務(wù)釋放鎖后,事務(wù)B獲取鎖得到余額稀颁,并對余額進(jìn)行計(jì)算更新成正確的值芬失,問題解決。

小結(jié)

本文通過一個更新丟失的例子介紹了InnoDB中事務(wù)回滾的原理匾灶,涉及一致性讀取棱烂,多版本并發(fā)控制,事務(wù)隔離級別以及鎖等內(nèi)容阶女。由于InnoDB中多版本并發(fā)控制及背后原理是比較難以理解的地方垢啼,因此主要對這部分內(nèi)容做了介紹窜锯。限于篇幅,對redo log的深入解析以及對InnoDB中其他方面的內(nèi)容并未提及芭析,例如InnoDB插入緩沖锚扎,雙寫,二進(jìn)制日志等特性馁启,InnoDB中的索引驾孔,InnoDB間隙鎖與下一個鍵鎖等其他鎖等。如對其他內(nèi)容有興趣或想深入了解InnoDB實(shí)現(xiàn)惯疙,推薦閱讀Mysql官方文檔以及《MySQL技術(shù)內(nèi)幕 InnoDB存儲引擎 》(第2版)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末翠勉,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子霉颠,更是在濱河造成了極大的恐慌对碌,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蒿偎,死亡現(xiàn)場離奇詭異朽们,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)诉位,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進(jìn)店門骑脱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人苍糠,你說我怎么就攤上這事叁丧。” “怎么了岳瞭?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵拥娄,是天一觀的道長。 經(jīng)常有香客問我瞳筏,道長条舔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任乏矾,我火速辦了婚禮孟抗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘钻心。我一直安慰自己凄硼,他們只是感情好贰您,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布良哲。 她就那樣靜靜地躺著,像睡著了一般芋簿。 火紅的嫁衣襯著肌膚如雪痒给。 梳的紋絲不亂的頭發(fā)上说墨,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天骏全,我揣著相機(jī)與錄音,去河邊找鬼尼斧。 笑死姜贡,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的棺棵。 我是一名探鬼主播楼咳,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼烛恤!你這毒婦竟也來了母怜?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤缚柏,失蹤者是張志新(化名)和其女友劉穎苹熏,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體币喧,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡轨域,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了粱锐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡扛邑,死狀恐怖怜浅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蔬崩,我是刑警寧澤恶座,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站沥阳,受9級特大地震影響跨琳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜桐罕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一脉让、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧功炮,春花似錦溅潜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至嫁怀,卻和暖如春设捐,著一層夾襖步出監(jiān)牢的瞬間借浊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工萝招, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蚂斤,地道東北人。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓即寒,卻偏偏與公主長得像橡淆,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子母赵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評論 2 348

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