MySQL 是如何實(shí)現(xiàn) ACID 的?
300本計(jì)算機(jī)編程的經(jīng)典書籍下載
AI全套:Python3+TensorFlow打造人臉識別智能小程序
最新人工智能資料-Google工程師親授 Tensorflow-入門到進(jìn)階
黑馬頭條項(xiàng)目 - Java Springboot2.0(視頻、資料、代碼和講義)14天完整版
來源:https://llc687.top/131.html
本文主要探討MySQL InnoDB 引擎下ACID的實(shí)現(xiàn)原理,對于諸如什么是事務(wù),隔離級別的含義等基礎(chǔ)知識不做過多闡述。
ACID
MySQL 作為一個關(guān)系型數(shù)據(jù)庫,以最常見的 InnoDB 引擎來說,是如何保證 ACID 的。
(Atomicity)原子性: 事務(wù)是最小的執(zhí)行單位,不允許分割。原子性確保動作要么全部完成,要么完全不起作用; (Consistency)一致性: 執(zhí)行事務(wù)前后,數(shù)據(jù)保持一致; (Isolation)隔離性: 并發(fā)訪問數(shù)據(jù)庫時(shí),一個事務(wù)不被其他事務(wù)所干擾。 (Durability)持久性: 一個事務(wù)被提交之后。對數(shù)據(jù)庫中數(shù)據(jù)的改變是持久的,即使數(shù)據(jù)庫發(fā)生故障。
隔離性
| 隔離級別 | 說明 |
|---|---|
| 讀未提交 | 一個事務(wù)還沒提交時(shí),它做的變更就能被別的事務(wù)看到 |
| 讀提交 | 一個事務(wù)提交之后,它做的變更才會被其他事務(wù)看到 |
| 可重復(fù)讀 | 一個事務(wù)中,對同一份數(shù)據(jù)的讀取結(jié)果總是相同的,無論是否有其他事務(wù)對這份數(shù)據(jù)進(jìn)行操作,以及這個事務(wù)是否提交。InnoDB默認(rèn)級別。 |
| 串行化 | 事務(wù)串行化執(zhí)行,每次讀都需要獲得表級共享鎖,讀寫相互都會阻塞,隔離級別最高,犧牲系統(tǒng)并發(fā)性。 |
不同的隔離級別是為了解決不同的問題。也就是臟讀、幻讀、不可重復(fù)讀。
| 隔離級別 | 臟讀 | 不可重復(fù)讀 | 幻讀 |
|---|---|---|---|
| 讀未提交 | 可以出現(xiàn) | 可以出現(xiàn) | 可以出現(xiàn) |
| 讀提交 | 不允許出現(xiàn) | 可以出現(xiàn) | 可以出現(xiàn) |
| 可重復(fù)讀 | 不允許出現(xiàn) | 不允許出現(xiàn) | 可以出現(xiàn) |
| 序列化 | 不允許出現(xiàn) | 不允許出現(xiàn) | 不允許出現(xiàn) |
鎖
粒度
從粒度上來說就是表鎖、頁鎖、行鎖。表鎖有意向共享鎖、意向排他鎖、自增鎖等。行鎖是在引擎層由各個引擎自己實(shí)現(xiàn)的。但并不是所有的引擎都支持行鎖,比如 MyISAM 引擎就不支持行鎖。
行鎖的種類
在 InnoDB 事務(wù)中,行鎖通過給索引上的索引項(xiàng)加鎖來實(shí)現(xiàn)。這意味著只有通過索引條件檢索數(shù)據(jù),InnoDB才使用行級鎖,否則將使用表鎖。行級鎖定同樣分為兩種類型:共享鎖和排他鎖,以及加鎖前需要先獲得的意向共享鎖和意向排他鎖。
共享鎖:讀鎖,允許其他事務(wù)再加S鎖,不允許其他事務(wù)再加X鎖,即其他事務(wù)只讀不可寫。
select...lock in share mode加鎖。排它鎖:寫鎖,不允許其他事務(wù)再加S鎖或者X鎖。
insert、update、delete、for update加鎖。
行鎖是在需要的時(shí)候才加上的,但并不是不需要了就立刻釋放,而是要等到事務(wù)結(jié)束時(shí)才釋放。這個就是兩階段鎖協(xié)議。
行鎖的實(shí)現(xiàn)算法
Record Lock
單個行記錄上的鎖,總是會去鎖住索引記錄。
Gap Lock
間隙鎖,想一下幻讀的原因,其實(shí)就是行鎖只能鎖住行,但新插入記錄這個動作,要更新的是記錄之間的“間隙”。所以加入間隙鎖來解決幻讀。
Next-Key Lock
Gap Lock + Record Lock, 左開又閉。
鎖之于隔離性
大致介紹了下鎖,可以看到。有了鎖,當(dāng)某事務(wù)正在寫數(shù)據(jù)時(shí),其他事務(wù)獲取不到寫鎖,就無法寫數(shù)據(jù),一定程度上保證了事務(wù)間的隔離。但前面說,加了寫鎖,為什么其他事務(wù)也能讀數(shù)據(jù)呢,不是獲取不到讀鎖嗎?
MVCC
版本鏈
Innodb 中行記錄的存儲格式,有一些額外的字段:DATA_TRX_ID和DATA_ROLL_PTR。
DATA_TRX_ID:數(shù)據(jù)行版本號。用來標(biāo)識最近對本行記錄做修改的事務(wù) id。 DATA_ROLL_PTR:指向該行回滾段的指針。該行記錄上所有舊版本,在 undo log中都通過鏈表的形式組織。
undo log : 記錄數(shù)據(jù)被修改之前的日志,后面會詳細(xì)說。

ReadView
在每一條 SQL 開始的時(shí)候被創(chuàng)建,有幾個重要屬性:
trx_ids: 當(dāng)前系統(tǒng)活躍(未提交)事務(wù)版本號集合。 low_limit_id: 創(chuàng)建當(dāng)前 read view 時(shí)“當(dāng)前系統(tǒng)最大事務(wù)版本號+1”。 up_limit_id: 創(chuàng)建當(dāng)前read view 時(shí)“系統(tǒng)正處于活躍事務(wù)最小版本號” creator_trx_id: 創(chuàng)建當(dāng)前read view的事務(wù)版本號;

開始查詢
現(xiàn)在開始查詢,一個 select 過來了,找到了一行數(shù)據(jù)。
DATA_TRX_ID <up_limit_id :說明數(shù)據(jù)在當(dāng)前事務(wù)之前就存在了,顯示。
DATA_TRX_ID >= low_limit_id:
說明該數(shù)據(jù)是在當(dāng)前read view 創(chuàng)建后才產(chǎn)生的,數(shù)據(jù)不顯示。
不顯示怎么辦,根據(jù) DATA_ROLL_PTR 從 undo log 中找到歷史版本,找不到就空。 up_limit_id <DATA_TRX_ID <low_limit_id :就要看隔離級別了。

RR 級別的幻讀
有了鎖和 MVCC , 事務(wù)的隔離性得到解決。這里要引申一下,默認(rèn)的 RR 的級別,解決了幻讀嗎?幻讀通常針對的是 INSERT, 不可重復(fù)度則針對 UPDATE 。
| 事物 1 | 事物 2 |
|---|---|
| begin | begin |
| select * from dept | |
| - | insert into dept(name) values("A") |
| - | commit |
| update dept set name="B" | |
| commit |
id name
1 A
2 B
實(shí)際卻是
id name
1 B
2 B
原子性
接著說說原子性。前文有提到 undo log ,回滾日志。隔離性的MVCC其實(shí)就是依靠它來實(shí)現(xiàn)的,原子性也是。實(shí)現(xiàn)原子性的關(guān)鍵,是當(dāng)事務(wù)回滾時(shí)能夠撤銷所有已經(jīng)成功執(zhí)行的sql語句。
當(dāng)事務(wù)對數(shù)據(jù)庫進(jìn)行修改時(shí),InnoDB會生成對應(yīng)的 undo log;如果事務(wù)執(zhí)行失敗或調(diào)用了 rollback,導(dǎo)致事務(wù)需要回滾,便可以利用 undo log 中的信息將數(shù)據(jù)回滾到修改之前的樣子。undo log 屬于邏輯日志,它記錄的是sql執(zhí)行相關(guān)的信息。當(dāng)發(fā)生回滾時(shí),InnoDB 會根據(jù) undo log 的內(nèi)容做與之前相反的工作:
對于每個 insert,回滾時(shí)會執(zhí)行 delete; 對于每個 delete,回滾時(shí)會執(zhí)行insert; 對于每個 update,回滾時(shí)會執(zhí)行一個相反的 update,把數(shù)據(jù)改回去。
持久性
Innnodb有很多 log,持久性靠的是 redo log。
一條SQL更新語句怎么運(yùn)行
redo log
redo log 就是這個粉板,當(dāng)有一條記錄要更新時(shí),InnoDB 引擎就會先把記錄寫到 redo log(并更新內(nèi)存),這個時(shí)候更新就算完成了。在適當(dāng)?shù)臅r(shí)候,將這個操作記錄更新到磁盤里面,而這個更新往往是在系統(tǒng)比較空閑的時(shí)候做,這就像打烊以后掌柜做的事。
redo log 有兩個特點(diǎn):
大小固定,循環(huán)寫 crash-safe
Buffer Pool
InnoDB還提供了緩存,Buffer Pool 中包含了磁盤中部分?jǐn)?shù)據(jù)頁的映射,作為訪問數(shù)據(jù)庫的緩沖:
當(dāng)讀取數(shù)據(jù)時(shí),會先從Buffer Pool中讀取,如果Buffer Pool中沒有,則從磁盤讀取后放入Buffer Pool; 當(dāng)向數(shù)據(jù)庫寫入數(shù)據(jù)時(shí),會首先寫入Buffer Pool,Buffer Pool中修改的數(shù)據(jù)會定期刷新到磁盤中。
Buffer Pool 的使用大大提高了讀寫數(shù)據(jù)的效率,但是也帶了新的問題:如果MySQL宕機(jī),而此時(shí) Buffer Pool 中修改的數(shù)據(jù)還沒有刷新到磁盤,就會導(dǎo)致數(shù)據(jù)的丟失,事務(wù)的持久性無法保證。
所以加入了 redo log。當(dāng)數(shù)據(jù)修改時(shí),除了修改Buffer Pool中的數(shù)據(jù),還會在redo log記錄這次操作;
當(dāng)事務(wù)提交時(shí),會調(diào)用fsync接口對redo log進(jìn)行刷盤。
如果MySQL宕機(jī),重啟時(shí)可以讀取redo log中的數(shù)據(jù),對數(shù)據(jù)庫進(jìn)行恢復(fù)。
redo log采用的是WAL(Write-ahead logging,預(yù)寫式日志),所有修改先寫入日志,再更新到Buffer Pool,保證了數(shù)據(jù)不會因MySQL宕機(jī)而丟失,從而滿足了持久性要求。而且這樣做還有兩個優(yōu)點(diǎn):
刷臟頁是隨機(jī) IO,redo log 順序 IO 刷臟頁以Page為單位,一個Page上的修改整頁都要寫;而redo log 只包含真正需要寫入的,無效 IO 減少。
binlog
說到這,可能會疑問還有個 bin log 也是寫操作并用于數(shù)據(jù)的恢復(fù),有啥區(qū)別呢。
層次:redo log 是 innoDB 引擎特有的,server 層的叫 binlog(歸檔日志) 內(nèi)容:redolog 是物理日志,記錄“在某個數(shù)據(jù)頁上做了什么修改”;binlog 是邏輯日志,是語句的原始邏輯,如“給 ID=2 這一行的 c 字段加 1 ” 寫入:redolog 循環(huán)寫且寫入時(shí)機(jī)較多,binlog 追加且在事務(wù)提交時(shí)寫入
binlog 和 redo log
對于語句 update T set c=c+1 where ID=2;
執(zhí)行器先找引擎取 ID=2 這一行。ID 是主鍵,直接用樹搜索找到。如果 ID = 2 這一行所在數(shù)據(jù)頁就在內(nèi)存中,就直接返回給執(zhí)行器;否則,需要先從磁盤讀入內(nèi)存,再返回。 執(zhí)行器拿到引擎給的行數(shù)據(jù),把這個值加上 1,N+1,得到新的一行數(shù)據(jù),再調(diào)用引擎接口寫入這行新數(shù)據(jù)。 引擎將這行新數(shù)據(jù)更新到內(nèi)存中,同時(shí)將這個更新操作記錄到 redo log 里面,此時(shí) redo log 處于 prepare 狀態(tài)。然后告知執(zhí)行器執(zhí)行完成了,隨時(shí)可以提交事務(wù)。 執(zhí)行器生成這個操作的 binlog,并把 binlog 寫入磁盤。 執(zhí)行器調(diào)用引擎的提交事務(wù)接口,引擎把剛剛寫入的 redo log 改成提交(commit)狀態(tài),更新完成
先 redo 后 bin : binlog 丟失,少了一次更新,恢復(fù)后仍是0。 先 bin 后 redo : 多了一次事務(wù),恢復(fù)后是1。
一致性
一致性是事務(wù)追求的最終目標(biāo),前問所訴的原子性、持久性和隔離性,其實(shí)都是為了保證數(shù)據(jù)庫狀態(tài)的一致性。當(dāng)然,上文都是數(shù)據(jù)庫層面的保障,一致性的實(shí)現(xiàn)也需要應(yīng)用層面進(jìn)行保障。
也就是你的業(yè)務(wù),比如購買操作只扣除用戶的余額,不減庫存,肯定無法保證狀態(tài)的一致。
總結(jié)
MySQL 都很熟, ACID 也知道是個啥,但 MySQL 的 ACID 怎么實(shí)現(xiàn)的?
有時(shí)候,就像你知道了有 undo log、redo log 但可能并不太清楚為什么有,當(dāng)知道了設(shè)計(jì)的目的,了解起來就會更加清晰了。另外,關(guān)注公眾號互聯(lián)網(wǎng)架構(gòu)師,在后臺回復(fù):2T,可以獲取我整理的 Java/ MySQL 系列面試題和答案,非常齊全。
參考:
https://zhuanlan.zhihu.com/p/52977862
https://learnku.com/articles/39212
https://www.cnblogs.com/rjzheng/p/10841031.html
https://www.cnblogs.com/kismetv/p/10331633.html
全棧架構(gòu)社區(qū)交流群
「全棧架構(gòu)社區(qū)」建立了讀者架構(gòu)師交流群,大家可以添加小編微信進(jìn)行加群。歡迎有想法、樂于分享的朋友們一起交流學(xué)習(xí)。
看完本文有收獲?請轉(zhuǎn)發(fā)分享給更多人
往期資源:
