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