1、前言
相信工作了一段時(shí)間的同學(xué)肯定都用過(guò)事務(wù),也都聽(tīng)說(shuō)過(guò)事務(wù)的4大特性ACID。ACID表示原子性、一致性、隔離性和持久性。一個(gè)很好的事務(wù)處理系統(tǒng),必須具備這些標(biāo)準(zhǔn)特性:
- 原子性(Atomicity):一個(gè)事務(wù)必須被視為一個(gè)不可分割的最小工作單元,整個(gè)事務(wù)中的所有操作要么全部提交成功,要么全部失敗回滾。
- 一致性(consistency):數(shù)據(jù)庫(kù)總是從一個(gè)一致性的狀態(tài)轉(zhuǎn)換到另一個(gè)一致性的狀態(tài)。(其實(shí)原子性和隔離性間接的保證了一致性)
- 隔離性(isolation):通常來(lái)說(shuō),一個(gè)事務(wù)所做的修改在最終提交以前,對(duì)其他事務(wù)是不可見(jiàn)的。
- 持久性(durability):一旦事務(wù)提交,則其所做的修改就會(huì)永久保存到數(shù)據(jù)庫(kù)中。
而我們最常說(shuō)的隔離性其實(shí)有對(duì)應(yīng)的隔離級(jí)別,MySQL規(guī)定的隔離級(jí)別有4種,分別是:
- READ UNCOMMITTED(讀未提交):在此級(jí)別里,事務(wù)的修改,即使沒(méi)有提交,對(duì)其他事務(wù)也都是可見(jiàn)的。事務(wù)可以讀取未提交的數(shù)據(jù),也就是會(huì)產(chǎn)生臟讀,在實(shí)際應(yīng)用中一般很少使用。
- READ COMMITTED(讀已提交):大多數(shù)數(shù)據(jù)庫(kù)系統(tǒng)的默認(rèn)隔離級(jí)別都是它,但是MySQL不是。它能夠避免臟讀問(wèn)題,但是在一個(gè)事務(wù)里對(duì)同一條數(shù)據(jù)的多次查詢可能會(huì)得到不同的結(jié)果,也就是會(huì)產(chǎn)生不可重復(fù)讀問(wèn)題。
- REPEATABLE READ(可重復(fù)讀):該隔離級(jí)別是MySQL默認(rèn)的隔離級(jí)別,看名字就知道它能夠防止不可重復(fù)讀問(wèn)題,但是在一個(gè)事務(wù)里對(duì)一段數(shù)據(jù)的多次讀取可能會(huì)導(dǎo)致不同的結(jié)果,也就是會(huì)有幻讀的問(wèn)題(注:這里說(shuō)的無(wú)法解決是MySQL定義層面,對(duì)于InnoDB引擎則完美的解決了幻讀的問(wèn)題,如果你正在使用InnoDB引擎,可忽略)
- SERIALIZABLE(可串行化):該隔離級(jí)別是級(jí)別最高的,它通過(guò)鎖來(lái)強(qiáng)制事務(wù)串行執(zhí)行,避免了前面說(shuō)的所有問(wèn)題。在高并發(fā)下,可能導(dǎo)致大量的超時(shí)和鎖爭(zhēng)用問(wèn)題。實(shí)際應(yīng)用中也很少用到這個(gè)隔離級(jí)別,因?yàn)镽R級(jí)別解決了所有問(wèn)題。
可以看到隔離級(jí)別里最重要的只有兩個(gè)隔離級(jí)別:RC和RR。那么問(wèn)題來(lái)了,我們知道上面說(shuō)的ACID以及隔離級(jí)別的實(shí)現(xiàn)原理嗎?無(wú)論是平時(shí)工作還是面試,這部分的問(wèn)題都重中之重,接下來(lái),我會(huì)拋出幾個(gè)問(wèn)題,大家可以帶著問(wèn)題來(lái)看此文:
ACID問(wèn)題:
- 為什么InnoDB能夠保證原子性?用的什么方式?
- 為什么InnoDB能夠保證一致性?用的什么方式?
- 為什么InnoDB能夠保證持久性?用的什么方式?
隔離性里隔離級(jí)別的問(wèn)題:
- 為什么RU級(jí)別會(huì)發(fā)生臟讀,而其他的隔離級(jí)別能夠避免?
- 為什么RC級(jí)別不能重復(fù)讀,而RR級(jí)別能夠避免?
- 為什么InnoDB的RR級(jí)別能夠防止幻讀?
解決這些問(wèn)題之前,我們要首先知道Redo log、Undo log以及MVCC都是什么。
2、Redo log
redo log(重做日志)用來(lái)實(shí)現(xiàn)事務(wù)的持久性,即事務(wù)ACID中的D。其由兩部分組成,一是內(nèi)存中的重做日志緩沖(redo log buffer),其實(shí)易失的。二是重做日志文件(redo log file),其是持久的。
在一個(gè)事務(wù)中的每一次SQL操作之后都會(huì)寫(xiě)入一個(gè)redo log到buffer中,在最后COMMIT的時(shí)候,必須先將該事務(wù)的所有日志寫(xiě)入到redo log file進(jìn)行持久化(這里的寫(xiě)入是順序?qū)懙模?,待事?wù)的COMMIT操作完成才算完成。

由于重做日志文件打開(kāi)沒(méi)有使用O_DIRECT選項(xiàng),因此重做日志緩沖先寫(xiě)入文件系統(tǒng)緩存。為了確保重做日志寫(xiě)入磁盤(pán),必須進(jìn)行一次fsync操作。由于fsync的效率取決于磁盤(pán)的性能,因此磁盤(pán)的性能決定了事務(wù)提交的性能,也就是數(shù)據(jù)庫(kù)的性能。由此我們可以得出在進(jìn)行批量操作的時(shí)候,不要for循環(huán)里面嵌套事務(wù)。
參數(shù) innodb_flush_log_at_trx_commit 用來(lái)控制重做日志刷新到磁盤(pán)的策略,該參數(shù)有3個(gè)值:0、1和2。
- 0:表示事務(wù)提交時(shí)不進(jìn)行寫(xiě)redo log file的操作,這個(gè)操作僅在master thread中完成(master thread每隔1秒進(jìn)行一次fsync操作)。
- 1:默認(rèn)值,表示每次事務(wù)提交時(shí)進(jìn)行寫(xiě)redo log file的操作。
- 2:表示事務(wù)提交時(shí)將redo log寫(xiě)入文件,不過(guò)僅寫(xiě)入文件系統(tǒng)的緩存中,不進(jìn)行fsync操作。
我們可以看到0和2的設(shè)置都比1的效率要高,但是破壞了數(shù)據(jù)庫(kù)的ACID特性,不建議使用!
對(duì)比binlog
在MySQL數(shù)據(jù)庫(kù)中還有一種二進(jìn)制日志(binlog),從表面上來(lái)看它和redo log很相似,都是記錄了對(duì)數(shù)據(jù)庫(kù)操作的日志,但是,它們有著非常大的不同。
首先,redo log是在MySQL的InnoDB引擎層產(chǎn)生,而binlog則是在MySQL的上層產(chǎn)生,它不僅針對(duì)InnoDB引擎,其他任何引擎對(duì)于數(shù)據(jù)庫(kù)的更改都會(huì)產(chǎn)生binlog。
其次,兩種日志記錄的內(nèi)容形式不同,binlog是一種邏輯日志,其記錄的是對(duì)應(yīng)的SQL語(yǔ)句。而redo log則是記錄的物理格式日志,其記錄的是對(duì)于每個(gè)頁(yè)的修改。
此外,兩種日志記錄寫(xiě)入磁盤(pán)的時(shí)間點(diǎn)不同,binlog只在事務(wù)提交完成后一次性寫(xiě)入,而redo log在上面也說(shuō)了是在事務(wù)進(jìn)行中不斷被寫(xiě)入,這表現(xiàn)為日志并不是隨事務(wù)提交的順序進(jìn)行寫(xiě)入的。

redo log block
在InnoDB引擎中,redo log都是以512字節(jié)進(jìn)行存儲(chǔ)的(和磁盤(pán)扇區(qū)的大小一樣,因此redo log寫(xiě)入可以保證原子性,不需要double write),也就是重做日志緩存和文件都是以塊的方式進(jìn)行保存的,稱為redo log block,每個(gè)block占512字節(jié)。
重做日志除了日志本身之外,還由日志塊頭(log block header)及日志塊尾(log block tailer)兩部分組成。

下面我來(lái)解釋一下組成Log Block header的4個(gè)部分各自的含義:
- LOG_BLOCK_HDR_NO:它主要用來(lái)標(biāo)記所處Redo Log Buffer中Log Block的位置。
- LOG_BLOCK_HDR_DATA_LEN:它表示Log Block所占用的大小。當(dāng)Log Block被寫(xiě)滿時(shí),該值為0x200,表示使用全部Log Block空間,即占用512字節(jié)。
- LOG_BLOCK_FIRST_REC_GROUP:表示Log Block中第一個(gè)日志所在的偏移量,如果該值大小和LOG_BLOCK_HDR_DATA_LEN相同,則表示當(dāng)前Log Block不包含新的日志,如果事務(wù)的日志大小超過(guò)一個(gè)Log Block的大小,剩余的將會(huì)接著保存到一個(gè)新的Log Block中。
- LOG_BLOCK_CHECKPOINT_NO:表示該Log Block最后被寫(xiě)入時(shí)的檢查點(diǎn)第4字節(jié)的值。
Log Block tailer只包含一個(gè)LOG_BLOCK_TRL_NO,它的值和LOG_BLOCK_HDR_NO相同,并在函數(shù)log_block_init中被初始化。
crash recovery
前面提到了redo log是用來(lái)實(shí)現(xiàn)ACID的持久性的,也就是只要事務(wù)提交成功后,事務(wù)內(nèi)的所有修改都會(huì)保存到數(shù)據(jù)庫(kù),哪怕這時(shí)候數(shù)據(jù)庫(kù)crash了,也要有辦法來(lái)進(jìn)行恢復(fù)。也就是Crash Recovery。
說(shuō)到恢復(fù),我們先來(lái)了解一個(gè)概念:什么是LSN?
LSN(log sequence number) 用于記錄日志序號(hào),它是一個(gè)不斷遞增的 unsigned long long 類型整數(shù),占用8字節(jié)。它代表的含義有:
- redo log寫(xiě)入的總量。
- checkpoint的位置。
- 頁(yè)的版本,用來(lái)判斷是否需要進(jìn)行恢復(fù)操作。
checkpoint:它是redo log中的一個(gè)檢查點(diǎn),這個(gè)點(diǎn)之前的所有數(shù)據(jù)都已經(jīng)刷新回磁盤(pán),當(dāng)DB crash后,通過(guò)對(duì)checkpoint之后的redo log進(jìn)行恢復(fù)就可以了。
我們可以通過(guò)命令show engine innodb status來(lái)觀察LSN的情況:
---
LOG
---
Log sequence number 33646077360
Log flushed up to 33646077360
Last checkpoint at 33646077360
0 pending log writes, 0 pending chkp writes
49687445 log i/o's done, 1.25 log i/o's/second
Log sequence number表示當(dāng)前的LSN,Log flushed up to表示刷新到redo log文件的LSN,Last checkpoint at表示刷新到磁盤(pán)的LSN。如果把它們?nèi)齻€(gè)簡(jiǎn)寫(xiě)為 A、B、C 的話,它們的值的大小肯定為 A>=B>=C。
InnoDB引擎在啟動(dòng)時(shí)不管上次數(shù)據(jù)庫(kù)運(yùn)行時(shí)是否正常關(guān)閉,都會(huì)進(jìn)行恢復(fù)操作。因?yàn)橹刈鋈罩居涗浀氖俏锢砣罩荆虼嘶謴?fù)的速度比邏輯日志,如二進(jìn)制日志要快很多。恢復(fù)的時(shí)候只需要找到redo log的checkpoint進(jìn)行恢復(fù)即可。

3、Undo log
重做日志記錄了事務(wù)的行為,可以很好的通過(guò)其對(duì)頁(yè)進(jìn)行“重做”操作。但是事務(wù)有時(shí)候還需要進(jìn)行回滾操作,也就是ACID中的A(原子性),這時(shí)就需要Undo log了。因此在數(shù)據(jù)庫(kù)進(jìn)行修改時(shí),InnoDB存儲(chǔ)引擎不但會(huì)產(chǎn)生Redo,還會(huì)產(chǎn)生一定量的Undo。這樣如果用戶執(zhí)行的事務(wù)或語(yǔ)句由于某種原因失敗了,又或者用戶一條ROLLBACK語(yǔ)句請(qǐng)求回滾,就可以利用這些Undo信息將數(shù)據(jù)庫(kù)回滾到修改之前的樣子。
Undo log是InnoDB MVCC事務(wù)特性的重要組成部分。當(dāng)我們對(duì)記錄做了變更操作時(shí)就會(huì)產(chǎn)生Undo記錄,Undo記錄默認(rèn)被記錄到系統(tǒng)表空間(ibdata)中,但從5.6開(kāi)始,也可以使用獨(dú)立的Undo 表空間。
Undo記錄中存儲(chǔ)的是老版本數(shù)據(jù),當(dāng)一個(gè)舊的事務(wù)需要讀取數(shù)據(jù)時(shí),為了能讀取到老版本的數(shù)據(jù),需要順著undo鏈找到滿足其可見(jiàn)性的記錄。當(dāng)版本鏈很長(zhǎng)時(shí),通??梢哉J(rèn)為這是個(gè)比較耗時(shí)的操作。
基本文件結(jié)構(gòu)
為了保證事務(wù)并發(fā)操作時(shí),在寫(xiě)各自的undo log時(shí)不產(chǎn)生沖突,InnoDB采用回滾段(Rollback Segment,簡(jiǎn)稱Rseg)的方式來(lái)維護(hù)undo log的并發(fā)寫(xiě)入和持久化。回滾段實(shí)際上是一種 Undo 文件組織方式,每個(gè)回滾段又有多個(gè)undo log slot。具體的文件組織方式如下圖所示:

上圖展示了基本的Undo回滾段布局結(jié)構(gòu),其中:
- rseg0預(yù)留在系統(tǒng)表空間ibdata中。
- rseg 1~rseg 32 這32個(gè)回滾段存放于臨時(shí)表的系統(tǒng)表空間中,用于臨時(shí)表的undo。
- rseg33~rseg 128 則根據(jù)配置(InnoDB >= 1.1默認(rèn)128,可通過(guò)參數(shù)
innodb_undo_logs設(shè)置)存放到獨(dú)立undo表空間中(如果沒(méi)有打開(kāi)獨(dú)立Undo表空間,則存放于ibdata中,獨(dú)立表空間可以通過(guò)參數(shù)innodb_undo_directory設(shè)置),用于普通事務(wù)的undo。
如圖所示,每個(gè)回滾段維護(hù)了一個(gè)段頭頁(yè),在該page中又劃分了1024個(gè)slot(TRX_RSEG_N_SLOTS),每個(gè)slot又對(duì)應(yīng)到一個(gè)undo log對(duì)象,因此理論上InnoDB最多支持 96 * 1024個(gè)普通事務(wù)。
Undo log的格式
在InnoDB引擎中,undo log分為:
- insert undo log
- update undo log
insert undo log是指在insert操作中產(chǎn)生的undo log,因?yàn)閕nsert操作的記錄,只對(duì)事務(wù)本身可見(jiàn),對(duì)其他事務(wù)不可見(jiàn)(這是事務(wù)隔離性的要求),故該undo log可以在事務(wù)提交后直接刪除,不需要進(jìn)行purge操作。而update undo log記錄的是delete和update操作產(chǎn)生的undo log。該undo log可能需要提供MVCC機(jī)制,因此不能在事務(wù)提交時(shí)就進(jìn)行刪除,提交時(shí)放入undo log鏈表,等待purge線程進(jìn)行最后的刪除。下面是兩種undo log的結(jié)構(gòu)圖。

purge
對(duì)于一條delete語(yǔ)句 delete from t where a = 1,如果列a有聚集索引,則不會(huì)進(jìn)行真正的刪除,而只是在主鍵列等于1的記錄delete flag設(shè)置為1,即記錄還是存在在B+樹(shù)中。而對(duì)于update操作,不是直接對(duì)記錄進(jìn)行更新,而是標(biāo)識(shí)舊記錄為刪除狀態(tài),然后新產(chǎn)生一條記錄。那這些舊版本標(biāo)識(shí)位刪除的記錄何時(shí)真正的刪除?怎么刪除?
其實(shí)InnoDB是通過(guò)undo日志來(lái)進(jìn)行舊版本的刪除操作的,在InnoDB內(nèi)部,這個(gè)操作被稱之為purge操作,原來(lái)在srv_master_thread主線程中完成,后來(lái)進(jìn)行優(yōu)化,開(kāi)辟了purge線程進(jìn)行purge操作,并且可以設(shè)置purge線程的數(shù)量。purge操作每10s進(jìn)行一次。
為了節(jié)省存儲(chǔ)空間,InnoDB存儲(chǔ)引擎的undo log設(shè)計(jì)是這樣的:一個(gè)頁(yè)上允許多個(gè)事務(wù)的undo log存在。雖然這不代表事務(wù)在全局過(guò)程中提交的順序,但是后面的事務(wù)產(chǎn)生的undo log總在最后。此外,InnoDB存儲(chǔ)引擎還有一個(gè)history列表,它根據(jù)事務(wù)提交的順序,將undo log進(jìn)行連接,如下面的一種情況:

在執(zhí)行purge過(guò)程中,InnoDB存儲(chǔ)引擎首先從history list中找到第一個(gè)需要被清理的記錄,這里為trx1,清理之后InnoDB存儲(chǔ)引擎會(huì)在trx1所在的Undo page中繼續(xù)尋找是否存在可以被清理的記錄,這里會(huì)找到事務(wù)trx3,接著找到trx5,但是發(fā)現(xiàn)trx5被其他事務(wù)所引用而不能清理,故再去history list中取查找,發(fā)現(xiàn)最尾端的記錄時(shí)trx2,接著找到trx2所在的Undo page,依次把trx6、trx4清理,由于Undo page2中所有的記錄都被清理了,因此該Undo page可以進(jìn)行重用。
InnoDB存儲(chǔ)引擎這種先從history list中找undo log,然后再?gòu)腢ndo page中找undo log的設(shè)計(jì)模式是為了避免大量隨機(jī)讀操作,從而提高purge的效率。
4、多版本控制MVCC
MVCC 多版本并發(fā)控制技術(shù),用于多事務(wù)環(huán)境下,對(duì)數(shù)據(jù)讀寫(xiě)在不加讀寫(xiě)鎖的情況下實(shí)現(xiàn)互不干擾,從而實(shí)現(xiàn)數(shù)據(jù)庫(kù)的隔離性,在事務(wù)隔離級(jí)別為Read Commit 和 Repeatable Read中使用到,今天我們就用最簡(jiǎn)單的方式,來(lái)分析下MVCC具體的原理,先解釋幾個(gè)概念。
InnoDB存儲(chǔ)引擎的行結(jié)構(gòu)
InnoDB表數(shù)據(jù)的組織方式為主鍵聚簇索引,二級(jí)索引中采用的是(索引鍵值, 主鍵鍵值)的組合來(lái)唯一確定一條記錄。
InnoDB表數(shù)據(jù)為主鍵聚簇索引,mysql默認(rèn)為每個(gè)索引行添加了4個(gè)隱藏的字段,分別是:
- DB_ROW_ID:InnoDB引擎中一個(gè)表只能有一個(gè)主鍵,用于聚簇索引,如果表沒(méi)有定義主鍵會(huì)選擇第一個(gè)非Null的唯一索引作為主鍵,如果還沒(méi)有,生成一個(gè)隱藏的DB_ROW_ID作為主鍵構(gòu)造聚簇索引。
- DB_TRX_ID:最近更改該行數(shù)據(jù)的事務(wù)ID。
- DB_ROLL_PTR:undo log的指針,用于記錄之前歷史數(shù)據(jù)在undo log中的位置。
- DELETE BIT:索引刪除標(biāo)志,如果DB刪除了一條數(shù)據(jù),是優(yōu)先通知索引將該標(biāo)志位設(shè)置為1,然后通過(guò)(purge)清除線程去異步刪除真實(shí)的數(shù)據(jù)。

整個(gè)MVCC的機(jī)制都是通過(guò)DB_TRX_ID,DB_ROLL_PTR這2個(gè)隱藏字段來(lái)實(shí)現(xiàn)的。
事務(wù)鏈表
當(dāng)一個(gè)事務(wù)開(kāi)始的時(shí)候,會(huì)將當(dāng)前數(shù)據(jù)庫(kù)中正在活躍的所有事務(wù)(執(zhí)行begin,但是還沒(méi)有commit的事務(wù))保存到一個(gè)叫trx_sys的事務(wù)鏈表中,事務(wù)鏈表中保存的都是未提交的事務(wù),當(dāng)事務(wù)提交之后會(huì)從其中刪除。

ReadView
有了前面隱藏列和事務(wù)鏈表的基礎(chǔ),接下去就可以構(gòu)造MySQL實(shí)現(xiàn)MVCC的關(guān)鍵——ReadView。
ReadView說(shuō)白了就是一個(gè)數(shù)據(jù)結(jié)構(gòu),在事務(wù)開(kāi)始的時(shí)候會(huì)根據(jù)上面的事務(wù)鏈表構(gòu)造一個(gè)ReadView,初始化方法如下:
// readview 初始化
// m_low_limit_id = trx_sys->max_trx_id;
// m_up_limit_id = !m_ids.empty() ? m_ids.front() : m_low_limit_id;
ReadView::ReadView()
:
m_low_limit_id(),
m_up_limit_id(),
m_creator_trx_id(),
m_ids(),
m_low_limit_no()
{
ut_d(::memset(&m_view_list, 0x0, sizeof(m_view_list)));
}
總共做了以下幾件事:
- 活躍事務(wù)鏈表(
trx_sys)中事務(wù)id最大的值被賦值給m_low_limit_id。 - 活躍事務(wù)鏈表中第一個(gè)值(也就是事務(wù)id最小)被賦值給
m_up_limit_id。 -
m_ids為事務(wù)鏈表。

通過(guò)該ReadView,新的事務(wù)可以根據(jù)查詢到的所有活躍事務(wù)記錄的事務(wù)ID來(lái)匹配能夠看見(jiàn)該記錄,從而實(shí)現(xiàn)數(shù)據(jù)庫(kù)的事務(wù)隔離,主要邏輯如下:
- 通過(guò)聚簇索引的行結(jié)構(gòu)中DB_TRX_ID隱藏字段可以知道最近被哪個(gè)事務(wù)ID修改過(guò)。
- 一個(gè)新的事務(wù)開(kāi)始時(shí)會(huì)根據(jù)事務(wù)鏈表構(gòu)造一個(gè)ReadView。
- 當(dāng)前事務(wù)根據(jù)ReadView中的數(shù)據(jù)去跟檢索到的每一條數(shù)據(jù)去校驗(yàn),看看當(dāng)前事務(wù)是不是能看到這條數(shù)據(jù)。
那么問(wèn)題來(lái)了,怎么來(lái)判斷可見(jiàn)性呢?我們來(lái)通過(guò)源碼一探究竟:
// 判斷數(shù)據(jù)對(duì)應(yīng)的聚簇索引中的事務(wù)id在這個(gè)readview中是否可見(jiàn)
bool changes_visible(
trx_id_t id, // 記錄的id
const table_name_t& name) const
MY_ATTRIBUTE((warn_unused_result))
{
ut_ad(id > 0);
// 如果當(dāng)前記錄id < 事務(wù)鏈表的最小值或者等于創(chuàng)建該readview的id就是它自己,那么是可見(jiàn)的
if (id < m_up_limit_id || id == m_creator_trx_id) {
return(true);
}
check_trx_id_sanity(id, name);
// 如果該記錄的事務(wù)id大于事務(wù)鏈表中的最大值,那么不可見(jiàn)
if (id >= m_low_limit_id) {
return(false);
// 如果事務(wù)鏈表是空的,那也是可見(jiàn)的
} else if (m_ids.empty()) {
return(true);
}
const ids_t::value_type* p = m_ids.data();
//判斷是否在ReadView中,如果在說(shuō)明在創(chuàng)建ReadView時(shí) 此條記錄還處于活躍狀態(tài)則不應(yīng)該查詢到,否則說(shuō)明創(chuàng)建ReadView是此條記錄已經(jīng)是不活躍狀態(tài)則可以查詢到
return(!std::binary_search(p, p + m_ids.size(), id));
}
總結(jié)一下可見(jiàn)性判斷邏輯:
- 當(dāng)檢索到的數(shù)據(jù)的事務(wù)ID小于事務(wù)鏈表中的最小值(數(shù)據(jù)行的DB_TRX_ID < m_up_limit_id)表示這個(gè)數(shù)據(jù)在當(dāng)前事務(wù)開(kāi)啟前就已經(jīng)被其他事務(wù)修改過(guò)了,所以是可見(jiàn)的。
- 當(dāng)檢索到的數(shù)據(jù)的事務(wù)ID表示的是當(dāng)前事務(wù)自己修改的數(shù)據(jù)(數(shù)據(jù)行的DB_TRX_ID = m_creator_trx_id) 時(shí),數(shù)據(jù)可見(jiàn)。
- 當(dāng)檢索到的數(shù)據(jù)的事務(wù)ID大于事務(wù)鏈表中的最大值(數(shù)據(jù)行的DB_TRX_ID >= m_low_limit_id) 表示這個(gè)數(shù)據(jù)在當(dāng)前事務(wù)開(kāi)啟后到下一次查詢之間又被其他的事務(wù)修改過(guò),那么就是不可見(jiàn)的。
- 如果事務(wù)鏈表為空,那么也是可見(jiàn)的,也就是當(dāng)前事務(wù)開(kāi)始的時(shí)候,沒(méi)有其他任意一個(gè)事務(wù)在執(zhí)行。
- 當(dāng)檢索到的數(shù)據(jù)的事務(wù)ID在事務(wù)鏈表中的最小值和最大值之間,從m_low_limit_id到m_up_limit_id進(jìn)行遍歷,取出DB_ROLL_PTR指針?biāo)赶虻幕貪L段的事務(wù)ID,把它賦值給
trx_id_current,然后從步驟1重新開(kāi)始判斷,這樣總能最后找到一個(gè)可用的記錄。
RC和RR隔離級(jí)別ReadView的實(shí)現(xiàn)方式
我們知道,RC隔離級(jí)別是能看到其他事務(wù)提交后的修改記錄的,也就是不可重復(fù)讀,但是RR隔離級(jí)別完美的避免了,但是它們都是使用的MVCC機(jī)制,那又為何有兩種截然不同的結(jié)果呢?其實(shí)我們看一下他們創(chuàng)建ReadView的區(qū)別就知道了。
- 在RC事務(wù)隔離級(jí)別下,每次語(yǔ)句執(zhí)行都關(guān)閉ReadView,然后重新創(chuàng)建一份ReadView。
- 在RR下,事務(wù)開(kāi)始后第一個(gè)讀操作創(chuàng)建ReadView,一直到事務(wù)結(jié)束關(guān)閉。
上面的總結(jié)英文版為:With REPEATABLE READ isolation level, the snapshot is based on the time when the first read operation is performed. With READ COMMITTEDisolation level, the snapshot is reset to the time of each consistent read operation.
來(lái)源自MySQL官網(wǎng):MySQL Glossary-glos_consistent_read
因?yàn)镽C每次查詢語(yǔ)句都創(chuàng)建一個(gè)新的ReadView,所以活躍的事務(wù)列表一直在變,也就導(dǎo)致如果事務(wù)B update提交了后事務(wù)A才進(jìn)行查詢,查詢的結(jié)果就是最新的行,也就是不可重復(fù)讀咯。而RR則一直用的事務(wù)開(kāi)始時(shí)創(chuàng)建的ReadView。
5、總結(jié)
還記得開(kāi)頭提到的問(wèn)題嗎?現(xiàn)在應(yīng)該能夠全部解決了。
為什么InnoDB能夠保證原子性A?用的什么方式?
其實(shí)這個(gè)在上面Undo log中已經(jīng)提及了。在事務(wù)里任何對(duì)數(shù)據(jù)的修改都會(huì)寫(xiě)一個(gè)Undo log,然后進(jìn)行數(shù)據(jù)的修改,如果出現(xiàn)錯(cuò)誤或者用戶需要回滾的時(shí)候可以利用Undo log的備份數(shù)據(jù)恢復(fù)到事務(wù)開(kāi)始之前的狀態(tài)。
為什么InnoDB能夠保證持久性?用的什么方式?
這個(gè)在上面Redo log中已經(jīng)提及了。在一個(gè)事務(wù)中的每一次SQL操作之后都會(huì)寫(xiě)入一個(gè)redo log到buffer中,在最后COMMIT的時(shí)候,必須先將該事務(wù)的所有日志寫(xiě)入到redo log file進(jìn)行持久化(這里的寫(xiě)入是順序?qū)懙模?,待事?wù)的COMMIT操作完成才算完成。即使COMMIT后數(shù)據(jù)庫(kù)有任何的問(wèn)題,在下次重啟后依然能夠通過(guò)redo log的checkpoint進(jìn)行恢復(fù)。也就是上面提到的crash recovery。
為什么InnoDB能夠保證一致性?用的什么方式?
在事務(wù)處理的ACID屬性中,一致性是最基本的屬性,其它的三個(gè)屬性都為了保證一致性而存在的。
首先回顧一下一致性的定義。所謂一致性,指的是數(shù)據(jù)處于一種有意義的狀態(tài),這種狀態(tài)是語(yǔ)義上的而不是語(yǔ)法上的。最常見(jiàn)的例子是轉(zhuǎn)帳。例如從帳戶A轉(zhuǎn)一筆錢(qián)到帳戶B上,如果帳戶A上的錢(qián)減少了,而帳戶B上的錢(qián)卻沒(méi)有增加,那么我們認(rèn)為此時(shí)數(shù)據(jù)處于不一致的狀態(tài)。
在數(shù)據(jù)庫(kù)實(shí)現(xiàn)的場(chǎng)景中,一致性可以分為數(shù)據(jù)庫(kù)外部的一致性和數(shù)據(jù)庫(kù)內(nèi)部的一致性。前者由外部應(yīng)用的編碼來(lái)保證,即某個(gè)應(yīng)用在執(zhí)行轉(zhuǎn)帳的數(shù)據(jù)庫(kù)操作時(shí),必須在同一個(gè)事務(wù)內(nèi)部調(diào)用對(duì)帳戶A和帳戶B的操作。如果在這個(gè)層次出現(xiàn)錯(cuò)誤,這不是數(shù)據(jù)庫(kù)本身能夠解決的,也不屬于我們需要討論的范圍。后者由數(shù)據(jù)庫(kù)來(lái)保證,即在同一個(gè)事務(wù)內(nèi)部的一組操作必須全部執(zhí)行成功(或者全部失?。_@就是事務(wù)處理的原子性。(上面說(shuō)過(guò)了是用Undo log來(lái)保證的)
但是,原子性并不能完全保證一致性。在多個(gè)事務(wù)并行進(jìn)行的情況下,即使保證了每一個(gè)事務(wù)的原子性,仍然可能導(dǎo)致數(shù)據(jù)不一致的結(jié)果,比如丟失更新問(wèn)題。
為了保證并發(fā)情況下的一致性,引入了隔離性,即保證每一個(gè)事務(wù)能夠看到的數(shù)據(jù)總是一致的,就好象其它并發(fā)事務(wù)并不存在一樣。用術(shù)語(yǔ)來(lái)說(shuō),就是多個(gè)事務(wù)并發(fā)執(zhí)行后的狀態(tài),和它們串行執(zhí)行后的狀態(tài)是等價(jià)的。
為什么RU級(jí)別會(huì)發(fā)生臟讀,而其他的隔離級(jí)別能夠避免?
RU級(jí)別的操作其實(shí)就是對(duì)事務(wù)內(nèi)的每一條更新語(yǔ)句對(duì)應(yīng)的行記錄加上讀寫(xiě)鎖來(lái)操作,而不把一個(gè)事務(wù)當(dāng)成一個(gè)整體來(lái)加鎖,所以會(huì)導(dǎo)致臟讀。但是RC和RR能夠通過(guò)MVCC來(lái)保證記錄只有在最后COMMIT后才會(huì)讓別的事務(wù)看到。
為什么RC級(jí)別不能重復(fù)讀,而RR級(jí)別能夠避免?
這個(gè)在上面的MVCC的最后說(shuō)到了,在RC事務(wù)隔離級(jí)別下,每次語(yǔ)句執(zhí)行都關(guān)閉ReadView,然后重新創(chuàng)建一份ReadView。而在RR下,事務(wù)開(kāi)始后第一個(gè)讀操作創(chuàng)建ReadView,一直到事務(wù)結(jié)束關(guān)閉。
為什么InnoDB的RR級(jí)別能夠防止幻讀?
這個(gè)是因?yàn)镽R隔離級(jí)別使用了Next-key Lock這么個(gè)東東,也就是Gap Lock+Record Lock的方式來(lái)進(jìn)行間隙鎖定,具體原理本章不深入討論,可以參考我的另一篇文章。