本文從一個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é)果:
那么為什么在事物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ì)解決了以下問題:
- 事務(wù)的回滾:如果事務(wù)失敗需要回滾,那么事務(wù)可以根據(jù)快照信息構(gòu)建行的早期版本占拍,從而保證事務(wù)要么成功略就,要么失敗的原子性
- 讀的性能:當(dāng)事務(wù)或者更新語句鎖住行記錄時,其他事務(wù)對行的普通讀不需要等待鎖的釋放晃酒,讀的性能得到提高
- 讀者過多引起的寫者饑餓問題:如果不采用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)有鎖被釋放涧狮。
不同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版)