來源:https://llc687.top/131.html
本文主要探討MySQL InnoDB 引擎下ACID的實作原理,對于諸如什么是事務,隔離級別的含義等基礎知識不做過多闡述,
ACID
MySQL 作為一個關系型資料庫,以最常見的 InnoDB 引擎來說,是如何保證 ACID 的,
- (Atomicity)原子性: 事務是最小的執行單位,不允許分割,原子性確保動作要么全部完成,要么完全不起作用;
- (Consistency)一致性: 執行事務前后,資料保持一致;
- (Isolation)隔離性: 并發訪問資料庫時,一個事務不被其他事務所干擾,
- (Durability)持久性: 一個事務被提交之后,對資料庫中資料的改變是持久的,即使資料庫發生故障,
隔離性
先說說隔離性,首先是四種隔離級別,
| 隔離級別 | 說明 |
|---|---|
| 讀未提交 | 一個事務還沒提交時,它做的變更就能被別的事務看到 |
| 讀提交 | 一個事務提交之后,它做的變更才會被其他事務看到 |
| 可重復讀 | 一個事務中,對同一份資料的讀取結果總是相同的,無論是否有其他事務對這份資料進行操作,以及這個事務是否提交,InnoDB默認級別, |
| 串行化 | 事務串行化執行,每次讀都需要獲得表級共享鎖,讀寫相互都會阻塞,隔離級別最高,犧牲系統并發性, |
不同的隔離級別是為了解決不同的問題,也就是臟讀、幻讀、不可重復讀,
| 隔離級別 | 臟讀 | 不可重復讀 | 幻讀 |
|---|---|---|---|
| 讀未提交 | 可以出現 | 可以出現 | 可以出現 |
| 讀提交 | 不允許出現 | 可以出現 | 可以出現 |
| 可重復讀 | 不允許出現 | 不允許出現 | 可以出現 |
| 序列化 | 不允許出現 | 不允許出現 | 不允許出現 |
那么不同的隔離級別,隔離性是如何實作的,為什么不同事物間能夠互不干擾? 答案是 鎖 和 MVCC,
鎖
先來說說鎖, MySQL 有多少鎖,
粒度
從粒度上來說就是表鎖、頁鎖、行鎖,
表鎖有意向共享鎖、意向排他鎖、自增鎖等,
行鎖是在引擎層由各個引擎自己實作的,但并不是所有的引擎都支持行鎖,比如 MyISAM 引擎就不支持行鎖,
行鎖的種類
在 InnoDB 事務中,行鎖通過給索引上的索引項加鎖來實作,這意味著只有通過索引條件檢索資料,InnoDB才使用行級鎖,否則將使用表鎖,
行級鎖定同樣分為兩種型別:共享鎖和排他鎖,以及加鎖前需要先獲得的意向共享鎖和意向排他鎖,
- 共享鎖:讀鎖,允許其他事務再加S鎖,不允許其他事務再加X鎖,即其他事務只讀不可寫,
select...lock in share mode加鎖, - 排它鎖:寫鎖,不允許其他事務再加S鎖或者X鎖,
insert、update、delete、for update加鎖,
行鎖是在需要的時候才加上的,但并不是不需要了就立刻釋放,而是要等到事務結束時才釋放,這個就是兩階段鎖協議,
行鎖的實作演算法
Record Lock
單個行記錄上的鎖,總是會去鎖住索引記錄,
Gap Lock
間隙鎖,想一下幻讀的原因,其實就是行鎖只能鎖住行,但新插入記錄這個動作,要更新的是記錄之間的“間隙”,所以加入間隙鎖來解決幻讀,
Next-Key Lock
Gap Lock + Record Lock, 左開又閉,
鎖之于隔離性
大致介紹了下鎖,可以看到,有了鎖,當某事務正在寫資料時,其他事務獲取不到寫鎖,就無法寫資料,一定程度上保證了事務間的隔離,但前面說,加了寫鎖,為什么其他事務也能讀資料呢,不是獲取不到讀鎖嗎?
MVCC
前面說到,有了鎖,當前事務沒有寫鎖就不能修改資料,但還是能讀的,而且讀的時候,即使該行資料其他事務已修改且提交,還是可以重復讀到同樣的值,這就是MVCC,多版本的并發控制,Multi-Version Concurrency Control,
版本鏈
Innodb 中行記錄的存盤格式,有一些額外的欄位:DATA_TRX_ID和DATA_ROLL_PTR,
- DATA_TRX_ID:資料行版本號,用來標識最近對本行記錄做修改的事務 id,
- DATA_ROLL_PTR:指向該行回滾段的指標,該行記錄上所有舊版本,在
undo log中都通過鏈表的形式組織,
undo log : 記錄資料被修改之前的日志,后面會詳細說,
另外,MySQL 系列面試題和答案全部整理好了,微信搜索?Java技術堆疊,在后臺發送:面試,?可以在線閱讀,

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

開始查詢
現在開始查詢,一個 select 過來了,找到了一行資料,
-
DATA_TRX_ID <up_limit_id :說明資料在當前事務之前就存在了,顯示,
-
DATA_TRX_ID >= low_limit_id:
說明該資料是在當前read view 創建后才產生的,資料不顯示,
- 不顯示怎么辦,根據 DATA_ROLL_PTR 從 undo log 中找到歷史版本,找不到就空,
-
up_limit_id <DATA_TRX_ID <low_limit_id :就要看隔離級別了,

RR 級別的幻讀
有了鎖和 MVCC , 事務的隔離性得到解決,這里要引申一下,默認的 RR 的級別,解決了幻讀嗎?
幻讀通常針對的是 INSERT, 不可重復度則針對 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
實際卻是
id name
1 B
2 B
其實在 MySQL 可重復讀的隔離級別中并不是完全解決了幻讀的問題,而是解決了讀資料情況下的幻讀問題,而對于修改的操作依舊存在幻讀問題,就是說 MVCC 對于幻讀的解決時不徹底的,
原子性
接著說說原子性,前文有提到 undo log ,回滾日志,隔離性的MVCC其實就是依靠它來實作的,原子性也是,
實作原子性的關鍵,是當事務回滾時能夠撤銷所有已經成功執行的sql陳述句,
當事務對資料庫進行修改時,InnoDB會生成對應的 undo log;如果事務執行失敗或呼叫了 rollback,導致事務需要回滾,便可以利用 undo log 中的資訊將資料回滾到修改之前的樣子,
undo log 屬于邏輯日志,它記錄的是sql執行相關的資訊,當發生回滾時,InnoDB 會根據 undo log 的內容做與之前相反的作業:
- 對于每個 insert,回滾時會執行 delete;
- 對于每個 delete,回滾時會執行insert;
- 對于每個 update,回滾時會執行一個相反的 update,把資料改回去,
以update操作為例:當事務執行update時,其生成的undo log中會包含被修改行的主鍵(以便知道修改了哪些行)、修改了哪些列、這些列在修改前后的值等資訊,回滾時便可以使用這些資訊將資料還原到update之前的狀態,
持久性
Innnodb有很多 log,持久性靠的是 redo log,
一條SQL更新陳述句怎么運行
持久性肯定和寫有關,MySQL 里經常說到的 WAL 技術,WAL 的全稱是 Write-Ahead Logging,它的關鍵點就是先寫日志,再寫磁盤,就像小店做生意,有個粉板,有個賬本,來客了先寫粉板,等不忙的時候再寫賬本,
redo log
redo log 就是這個粉板,當有一條記錄要更新時,InnoDB 引擎就會先把記錄寫到 redo log(并更新記憶體),這個時候更新就算完成了,在適當的時候,將這個操作記錄更新到磁盤里面,而這個更新往往是在系統比較空閑的時候做,這就像打烊以后掌柜做的事,
redo log 有兩個特點
- 大小固定,回圈寫
- crash-safe
對于redo log 是有兩階段的:commit 和 prepare
如果不使用“兩階段提交”,資料庫的狀態就有可能和用它的日志恢復出來的庫的狀態不一致.
好了,先到這里,看看另一個,
Buffer Pool
InnoDB還提供了快取,Buffer Pool 中包含了磁盤中部分資料頁的映射,作為訪問資料庫的緩沖:
- 當讀取資料時,會先從Buffer Pool中讀取,如果Buffer Pool中沒有,則從磁盤讀取后放入Buffer Pool;
- 當向資料庫寫入資料時,會首先寫入Buffer Pool,Buffer Pool中修改的資料會定期重繪到磁盤中,
Buffer Pool 的使用大大提高了讀寫資料的效率,但是也帶了新的問題:如果MySQL宕機,而此時 Buffer Pool 中修改的資料還沒有重繪到磁盤,就會導致資料的丟失,事務的持久性無法保證,
所以加入了 redo log,
當資料修改時,除了修改Buffer Pool中的資料,還會在redo log記錄這次操作;
當事務提交時,會呼叫fsync介面對redo log進行刷盤,
如果MySQL宕機,重啟時可以讀取redo log中的資料,對資料庫進行恢復,
redo log采用的是WAL(Write-ahead logging,預寫式日志),所有修改先寫入日志,再更新到Buffer Pool,保證了資料不會因MySQL宕機而丟失,從而滿足了持久性要求,
而且這樣做還有兩個優點:
- 刷臟頁是隨機 IO,redo log 順序 IO
- 刷臟頁以Page為單位,一個Page上的修改整頁都要寫;而redo log 只包含真正需要寫入的,無效 IO 減少,
binlog
說到這,可能會疑問還有個 bin log 也是寫操作并用于資料的恢復,有啥區別呢,
- 層次:redo log 是 innoDB 引擎特有的,server 層的叫 binlog(歸檔日志)
- 內容:redolog 是物理日志,記錄“在某個資料頁上做了什么修改”;binlog 是邏輯日志,是陳述句的原始邏輯,如“給 ID=2 這一行的 c 欄位加 1 ”
- 寫入:redolog 回圈寫且寫入時機較多,binlog 追加且在事務提交時寫入
binlog 和 redo log
對于陳述句 update T set c=c+1 where ID=2;
- 執行器先找引擎取 ID=2 這一行,ID 是主鍵,直接用樹搜索找到,如果 ID = 2 這一行所在資料頁就在記憶體中,就直接回傳給執行器;否則,需要先從磁盤讀入記憶體,再回傳,
- 執行器拿到引擎給的行資料,把這個值加上 1,N+1,得到新的一行資料,再呼叫引擎介面寫入這行新資料,
- 引擎將這行新資料更新到記憶體中,同時將這個更新操作記錄到 redo log 里面,此時 redo log 處于 prepare 狀態,然后告知執行器執行完成了,隨時可以提交事務,
- 執行器生成這個操作的 binlog,并把 binlog 寫入磁盤,
- 執行器呼叫引擎的提交事務介面,引擎把剛剛寫入的 redo log 改成提交(commit)狀態,更新完成
為什么先寫 redo log 呢 ?
- 先 redo 后 bin : binlog 丟失,少了一次更新,恢復后仍是0,
- 先 bin 后 redo : 多了一次事務,恢復后是1,
一致性
一致性是事務追求的最終目標,前問所訴的原子性、持久性和隔離性,其實都是為了保證資料庫狀態的一致性,
當然,上文都是資料庫層面的保障,一致性的實作也需要應用層面進行保障,
也就是你的業務,比如購買操作只扣除用戶的余額,不減庫存,肯定無法保證狀態的一致,
總結
MySQL 都很熟, ACID 也知道是個啥,但 MySQL 的 ACID 怎么實作的?
有時候,就像你知道了有 undo log、redo log 但可能并不太清楚為什么有,當知道了設計的目的,了解起來就會更加清晰了,另外,關注公眾號Java技術堆疊,在后臺回復:面試,可以獲取我整理的 Java 系列面試題和答案,非常齊全,
參考:
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
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2021最新版)
2.別在再滿屏的 if/ else 了,試試策略模式,真香!!
3.臥槽!Java 中的 xx ≠ null 是什么新語法?
4.Spring Boot 2.5 重磅發布,黑暗模式太炸了!
5.《Java開發手冊(嵩山版)》最新發布,速速下載!
覺得不錯,別忘了隨手點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/301157.html
標籤:Java
