阿里面試官:MySQL是如何實(shí)現(xiàn)ACID的?
作為二本上岸大廠的后端應(yīng)屆生,深知沒(méi)人帶一路摸索的艱辛,想把自己的心路歷程與經(jīng)驗(yàn)心得收獲分享給大家。后期大廠面試系列持續(xù)更新中.....
1前文
之前有同學(xué)在面阿里二面被問(wèn)到:MYSQL是如何實(shí)現(xiàn)ACID的?其實(shí),如果叫簡(jiǎn)單介紹什么是ACID,大家肯定都能回答,但是,想要答好底層如何實(shí)現(xiàn)ACID特性的,還得考考功力啦!
今天,筆者簡(jiǎn)單談?wù)勛约簩?duì)ACID特性實(shí)現(xiàn)原理的理解。本文主要探討MYSQL InnoDB引擎下的ACID實(shí)現(xiàn)原理,對(duì)什么是事務(wù),隔離級(jí)別簡(jiǎn)單回顧一下。
2事務(wù)與ACID
何為事務(wù)呢?書(shū)上給予的概念多而難于理解,筆者對(duì)事務(wù)的理解:一系列操作組成,要么全部成功,要么全部失敗。它具備ACID四大特性,在并發(fā)下,可能存在臟讀、幻讀、不可重復(fù)讀的并發(fā)問(wèn)題,于是又引出了四大隔離級(jí)別。
01事務(wù)ACID特性
MYSQL作為一個(gè)關(guān)系型數(shù)據(jù)庫(kù),以最常見(jiàn)的InnoDB引擎來(lái)說(shuō),是如何保證ACID的。
(Atomicity)原子性:一些列操作要么全部成功,要么全部失敗
(Isolation)隔離性:事務(wù)的結(jié)果只有提交了其他事務(wù)才可見(jiàn)
(Consistency)一致性:數(shù)據(jù)庫(kù)總時(shí)從一個(gè)一致?tīng)顟B(tài)變到另一個(gè)一致?tīng)顟B(tài)(事務(wù)修改前后的數(shù)據(jù)總體保證一致 轉(zhuǎn)賬)
(Durability)持久性:事務(wù)提交后,對(duì)數(shù)據(jù)修改永久的
02原子性
在聊原子性之前,我得先給大家普及一個(gè)東西——undo log,這是啥玩意兒呢?如果想要詳細(xì)了解或則想知道它具體內(nèi)部咋實(shí)現(xiàn)的可以仔細(xì)去看書(shū),這里我就簡(jiǎn)單分享我的理解,知道這些,面試基本夠用啦。
undo log,它是一種回滾日志,既可以用來(lái)實(shí)現(xiàn)隔離性MVCC,也可以保證原子性。MVCC待會(huì)談?wù)?。?shí)現(xiàn)原子性的關(guān)鍵,是事務(wù)回滾時(shí)能夠撤銷(xiāo)所有已經(jīng)成功執(zhí)行的sql語(yǔ)句。
當(dāng)事務(wù)對(duì)數(shù)據(jù)庫(kù)進(jìn)行修改時(shí),InnoDB會(huì)生成對(duì)應(yīng)的undo log,undo log會(huì)保存事務(wù)開(kāi)始前老版本的數(shù)據(jù),當(dāng)事務(wù)發(fā)生異常,便會(huì)rollback回滾到老版本狀態(tài)。當(dāng)發(fā)生回滾時(shí),InnoDB會(huì)根據(jù)undo log的內(nèi)容做相反邏輯操作。
- insert語(yǔ)句,回滾時(shí)會(huì)執(zhí)行 delete;
- delete語(yǔ)句,回滾時(shí)會(huì)執(zhí)行insert;
- update語(yǔ)句,回滾時(shí)便執(zhí)行相反的update,把數(shù)據(jù)改回來(lái)。
總之,MYSQL的原子性便是由undo log來(lái)保證,undo log的作用我做了一下歸納總結(jié):
作用:undolog記錄事務(wù)開(kāi)始前老版本數(shù)據(jù),用于實(shí)現(xiàn)回滾,保證原子性,實(shí)現(xiàn)MVCC,會(huì)將數(shù)據(jù)修改前的舊版本保存在undolog,然后行記錄有個(gè)隱藏字段回滾指針指向老版本。
03持久性
在聊持久性之前,我們得先知道redo log。老規(guī)矩,想深入學(xué)習(xí)理解看書(shū)噢,這里只做筆者面試回答分享。
我們以一個(gè)生活小案例來(lái)理解一下下:
redo log,是一種物理日志。它類(lèi)似于一個(gè)卸貨的小推車(chē),我們卸貨若是每下一件物品就拿著去入庫(kù),那豈不是特浪費(fèi)時(shí)間(效率低、還要找到合適存庫(kù)位置)。此時(shí),若有一個(gè)小推車(chē),我們將貨物首先存放在小推車(chē),當(dāng)推車(chē)滿了再往庫(kù)里存,豈不大大增加了效率。
MYSQL中也用了類(lèi)似思想,我們?cè)俑聰?shù)據(jù)庫(kù)時(shí),先將更新操作記錄在redo log日志,等redo log滿了或則MYSQL空閑了再刷盤(pán)。
其實(shí)就是MySQL里經(jīng)常說(shuō)到的WAL技術(shù),WAL的全稱(chēng)是Write-Ahead Logging,它的關(guān)鍵點(diǎn)就是先寫(xiě)日志,再寫(xiě)磁盤(pán),也就是先裝小推車(chē),等不忙的時(shí)候再裝庫(kù)。
總之,MYSQL的持久性便是由redo log來(lái)保證,redo log的作用我做了一下歸納總結(jié):
redo log
物理日志
作用:會(huì)記錄事務(wù)開(kāi)啟后對(duì)數(shù)據(jù)做的修改,crash-safe
特性:空間一定,寫(xiě)完后會(huì)循環(huán)寫(xiě),有兩個(gè)指針write pos指向當(dāng)前記錄位置,checkpoint指向?qū)⒉脸奈恢茫瑀edolog相當(dāng)于是個(gè)取貨小車(chē),貨物太多時(shí)來(lái)不及一件一件入庫(kù)太慢了這樣,就先將貨物放入小車(chē),等到貨物不多或則小車(chē)滿了或則店里空閑時(shí)再將小車(chē)貨物送到庫(kù)房。用于crash-safe,數(shù)據(jù)庫(kù)異常斷電等情況可用redo log恢復(fù)。
以下只作了解:
寫(xiě)入流程:先寫(xiě)redo log buffer,然后wite到文件系統(tǒng)的page cache,此時(shí)并沒(méi)有持久化,然后fsync持久化到磁盤(pán)
寫(xiě)入策略:根據(jù)innodb_flush_log_at_trx_commit參數(shù)控制(我的記憶:innodb以事務(wù)的什么提交方式刷新日志)
0——>事務(wù)提交時(shí)只把redo log留在redo log buffer
1——>將redo log直接持久化到磁盤(pán)(所以有個(gè)雙“1”配置,后面會(huì)講)
2——>只是把redo log寫(xiě)到page cache
04隔離性
說(shuō)到隔離性,我們都知道MYSQL有四種隔離級(jí)別,用來(lái)解決存在的并發(fā)問(wèn)題。臟讀、幻讀、不可重復(fù)讀。
那么不同隔離級(jí)別,隔離性是怎樣實(shí)現(xiàn)的呢?具體實(shí)現(xiàn)原理是怎樣的呢?接下來(lái)我們就談?wù)劊床欢疀](méi)關(guān)系,老規(guī)矩,結(jié)尾會(huì)進(jìn)行總結(jié)滴!
一句話:鎖+MVCC。
鎖
1、表鎖
- lock table table_name read/write
- myisam執(zhí)行select自動(dòng)加讀鎖,執(zhí)行update/delete/insert自動(dòng)加寫(xiě)鎖
- 表加了讀鎖,不會(huì)阻塞其他線程的讀操作,阻塞寫(xiě)操作
- 表加了寫(xiě)鎖,讀寫(xiě)操作都阻塞
2、行鎖
鎖的類(lèi)型
- 間隙鎖-gap lock:鎖定區(qū)間范圍,防止幻讀,左開(kāi)右開(kāi),只在可重復(fù)讀隔離級(jí)別下生效—|—為了阻止多個(gè)事務(wù)將記錄插入到同一范圍內(nèi),而這會(huì)導(dǎo)致幻讀問(wèn)題的產(chǎn)生
- 記錄鎖-record Lock:鎖定行記錄,索的索引,索引失效,為表鎖
- 臨鍵鎖-next-key Lock:record lock+gap lock 左開(kāi)右閉(解決幻讀)
鎖的模式
- select .... for update
- 持有寫(xiě)鎖,別的不可加讀鎖,也不可加寫(xiě)鎖
- select .... lock in share mode
- 持有讀鎖,別的可以再加讀鎖,不可加寫(xiě)鎖
- 共享鎖-讀鎖-S鎖
- 排他鎖-寫(xiě)鎖-X鎖
- 意向鎖:讀意向鎖+寫(xiě)意向鎖
- 自增鎖
需要的時(shí)候加上,并不是馬上釋放,等事務(wù)提交才釋放,兩階段鎖協(xié)議
3、全局鎖——全庫(kù)邏輯備份
4、死鎖
- 兩個(gè)或多個(gè)事務(wù)在同一資源上相互占用,并請(qǐng)求加鎖時(shí),造成相互等待,無(wú)限阻塞
- innodb回滾擁有最少排他行級(jí)鎖的事務(wù)
- 設(shè)置鎖等待超時(shí)時(shí)間
樂(lè)觀鎖與悲觀鎖
- 悲觀鎖用數(shù)據(jù)庫(kù)自帶鎖機(jī)制——寫(xiě)多
- 樂(lè)觀鎖用version版本機(jī)制或CAS算法——讀多寫(xiě)少,很少發(fā)生沖突情況
MVCC
是什么:多版本并發(fā)控制。
原理提煉總結(jié):使用版本鏈+Read View
詳解:
版本鏈:同一行數(shù)據(jù)可能有多個(gè)版本
innodb數(shù)據(jù)表每行數(shù)據(jù)記錄會(huì)有幾個(gè)隱藏字段,row_id,事務(wù)ID,回滾指針。
1、Innodb采用主鍵索引(聚簇索引),會(huì)利用主鍵維護(hù)索引,若表沒(méi)有主鍵,就用第一個(gè)非空唯一索引,若沒(méi)有唯一索引,則用row_id這個(gè)隱藏字段作為主鍵索引。
2、事務(wù)開(kāi)啟會(huì)向系統(tǒng)申請(qǐng)一個(gè)事務(wù)ID,嚴(yán)格遞增,會(huì)向行記錄插入最近操作它的那個(gè)事務(wù)的ID
3、undolog會(huì)記錄事務(wù)前老版本數(shù)據(jù),然后行記錄中回滾指針會(huì)指向老版本位置,如此形成一條版本鏈。因此可以利用undo log實(shí)現(xiàn)回滾,保證原子性,同時(shí)用于實(shí)現(xiàn)MVCC版本鏈。
圖3 版本鏈形成
Read View讀已提交隔離級(jí)別下,會(huì)在每次查詢(xún)都生成一個(gè)Read View,可重讀讀只在事務(wù)開(kāi)始時(shí)生成一個(gè)Read View,以后每次查詢(xún)都用這個(gè)Read View,以此實(shí)現(xiàn)不同隔離界別。
Read View里面包含些什么?(一致性視圖)
一個(gè)數(shù)組+up_limit_id(低水位)+low_limit_id(高水位)(這里的up,low沒(méi)寫(xiě)錯(cuò),就是這么定義的)
1、數(shù)組里包含事務(wù)啟動(dòng)時(shí)當(dāng)前活躍事務(wù)ID(未提交事務(wù)),低水位就是活躍事務(wù)最小ID,高水位就是下一次將分配的事務(wù)ID,也就是目前最大事務(wù)ID+1。
數(shù)據(jù)可見(jiàn)性規(guī)則是怎樣實(shí)現(xiàn)的?
數(shù)據(jù)版本的可見(jiàn)性規(guī)則,就是基于數(shù)據(jù)的row trx_id和這個(gè)一致性視圖(Read View)的對(duì)比結(jié)果得到的。
視圖數(shù)組把所有的trx_id 分成了幾種不同的情況
圖4 數(shù)據(jù)版本可見(jiàn)性規(guī)則
讀取原理:
某事務(wù)T要訪問(wèn)數(shù)據(jù)A,先獲取該數(shù)據(jù)A中的事務(wù)id(獲取最近操作它的事務(wù)的事務(wù)ID),對(duì)比該事務(wù)T啟動(dòng)時(shí)刻生成的readview:
1、如果在readview的左邊(比readview都小),表示這個(gè)事務(wù)可以訪問(wèn)這數(shù)據(jù)(在左邊意味著該事務(wù)已經(jīng)提交)
2、如果在readview的右邊(比readview都大),表示這個(gè)版本是由將來(lái)啟動(dòng)的事務(wù)生成的,是肯定不可見(jiàn)的;
3、如果當(dāng)前事務(wù)在未提交事務(wù)集合中:
a、若 row trx_id在數(shù)組中,表示這個(gè)版本是由還沒(méi)提交的事務(wù)生成的,不可見(jiàn);
b. 若 row trx_id不在數(shù)組中,表示這個(gè)版本是已經(jīng)提交了的事務(wù)生成的,可見(jiàn)。
不可以訪問(wèn),獲取roll_pointer,通過(guò)版本鏈取上一版本。
根據(jù)數(shù)據(jù)歷史版本事務(wù)ID再重新與視圖數(shù)組對(duì)比。
這樣執(zhí)行下來(lái),雖然期間這一行數(shù)據(jù)被修改過(guò),但是事務(wù)A不論在什么時(shí)候查詢(xún),看到這行數(shù)據(jù)的結(jié)果都是一致的,所以我們稱(chēng)之為一致性讀。
總之,MYSQL的隔離性便是由MVCC+鎖來(lái)保證,各個(gè)隔離級(jí)別實(shí)現(xiàn)原理我做了一下歸納總結(jié):
隔離級(jí)別原理及解決問(wèn)題分析:
- 讀未提交:原理:直接讀取數(shù)據(jù),不能解決任何并發(fā)問(wèn)題
- 讀已提交:讀操作不加鎖,寫(xiě)操作加排他鎖,解決了臟讀。原理:利用MVCC實(shí)現(xiàn),每一句語(yǔ)句執(zhí)行前都會(huì)生成Read View(一致性視圖)
- 可重復(fù)讀:MVCC實(shí)現(xiàn),只有事務(wù)開(kāi)始時(shí)會(huì)創(chuàng)建Read View,之后事務(wù)里的其他查詢(xún)都用這個(gè)Read View。解決了臟讀、不可重復(fù)讀,快照讀(普通查詢(xún),讀取歷史數(shù)據(jù))使用MVCC解決了幻讀,當(dāng)前讀(讀取最新提交數(shù)據(jù))通過(guò)間隙鎖解決幻讀(lock in share mode、for update、update、detete、insert),間隙鎖在可重復(fù)讀下才生效。(默認(rèn)隔離級(jí)別)
- 可串行化:原理:使用鎖,讀加共享鎖,寫(xiě)加排他鎖,串行執(zhí)行
總結(jié):讀已提交和可重復(fù)讀實(shí)現(xiàn)原理就是MVCC Read View不同的生成時(shí)機(jī)。可重復(fù)讀只在事務(wù)開(kāi)始時(shí)生成一個(gè)Read View,之后都用的這個(gè);讀已提交每次執(zhí)行前都會(huì)生成Read View
05一致性
一致性是事務(wù)追求的最終目標(biāo),前問(wèn)所訴的原子性、持久性和隔離性,其實(shí)都是為了保證數(shù)據(jù)庫(kù)狀態(tài)的一致性。
當(dāng)然,上文都是數(shù)據(jù)庫(kù)層面的保障,一致性的實(shí)現(xiàn)也需要應(yīng)用層面進(jìn)行保障。也就是你的業(yè)務(wù),比如購(gòu)買(mǎi)操作只扣除用戶(hù)的余額,不減庫(kù)存,肯定無(wú)法保證狀態(tài)的一致。
你把周?chē)娜丝醋髂Ч?,你就生活在地獄;你把周?chē)娜丝醋魈焓?,你就生活在天堂?/p>
本文轉(zhuǎn)載自微信公眾號(hào)「小龍coding」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系小龍coding公眾號(hào)。