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