跟面試官侃半小時MySQL事務隔離性,從基本概念深入到實現
- 2020 年 3 月 27 日
- 筆記
提到MySQL的事務,我相信對MySQL有了解的同學都能聊上幾句,無論是面試求職,還是日常開發,MySQL的事務都跟我們息息相關。
而事務的ACID(即原子性Atomicity、一致性Consistency、隔離性Isolation、持久性Durability)可以說涵蓋了事務的全部知識點,所以,我們不僅要知道ACID是什麼,還要了解ACID背後的實現,只有這樣,無論在日常開發還是面試求職,都能無往而不利。
為了大家更好的閱讀體驗,對ACID的深入分析將分為上下兩篇。
本篇為上篇,主要圍繞ACID中的I,也就是“隔離性”展開,從基本概念,到隔離性的實現,最後以一個實戰案例進行融會貫通。
嗯,看完你都能理解,那跟面試官侃半小時隔離性就沒問題了。
1.事務隔離性的基本概念
1.1 什麼是ACID中的Isolation,隔離性
Isolation,隔離性,也有人稱之為並發控制(concurrency control)。事務的隔離性要求每個事務讀寫的對象對其他事務都是相互隔離的,也就是這個事務提交前,這個事務的修改內容對其他事務都是不可見的。事務的隔離性,主要是解決不同事物之間的相互讀寫影響。
所謂的讀寫影響注意分為三種:
- 臟讀:讀到了別的事務尚未提交(commit)的變更,別人沒提交,我讀到了。
- 不可重複讀:別的事務提交了變更,被當前事務讀到了。然後導致本事務多次select的結果不一樣,讀到了別的事務提交的內容。
- 幻讀:也是讀到了別的事務提交的內容,但是跟上面的不同之處在於,讀到了原本不存在的記錄。
注意,不可重複讀,主要是讀到了別的事務update的內容。而幻讀,是讀到了別的事務insert的內容。
1.2 隔離性的隔離級別
為了解決事務隔離性的問題,數據庫一般會有不同的隔離級別來解決相應的讀寫影響。
- 讀未提交:一個事務B還沒提交,它的修改就被別的事務A讀到了。
- 讀已提交:一個事務B提交後,它的修改被其他事務A看到了。
- 可重複讀:一個事物B提交前和提交後,事務A都無法讀到事務B的變更。
- 串行化:對同一行記錄,當出現不同事物的讀寫衝突時,是通過串行化的方式解決的,後一個事務必須等前一個事務完成才能執行。
不同隔離級別能夠解決不同的隔離性問題。
需要注意的是,這是標準事務隔離級別的定義。在MySQL的innodb引擎中,在可重複讀級別下,通過mvcc解決了幻讀的問題,具體實現我們後面再講。
同時,需要注意的是,到目前為止,我們說的讀,都是”快照讀”,普通的select。後面我們還會提到“當前讀”,是不一樣的哦。
2.事務隔離性的實現
要實現事務的隔離性,需要了解兩個方面的內容,一個是鎖,一個是多版本並發控制(MVCC)。
2.1 事務的行鎖
InnoDB中,實現了兩種標準的行級鎖:
- 共享鎖(S Lock),也叫讀鎖,允許事務讀取一行數據。
- 排它鎖(X Lock),也叫寫鎖,允許事務刪除或者更新一行數據(注意,這裡沒有提到插入哦,插入涉及到幻讀,可以看文章最後的說明)
普通select語句不會有任何鎖,那麼如何獲得共享鎖和排它鎖呢?
- Select … lock in share mode語句能夠獲得共享鎖
- Select … for update(特殊的select,用mysql簡單實現分佈式鎖經常用它)、Update、delete語句能夠獲得排它鎖
當一個事務A已經獲得了行r的共享鎖,那麼另一個事務B可以立刻獲得行r的共享鎖,因為不會改變r的數值,這種叫做鎖兼容。
如果這時候有事務C希望獲得行r的排它鎖,那麼就必須等待事務A和事務B釋放行r的共享鎖之後,才能獲得排它鎖,這種叫做鎖不兼容。
普通的select不會對行上鎖,而select…lock in share mode會上共享鎖,select…for update會上排它鎖。
- 對於普通的select的讀取方式,稱為”快照讀“,也叫”一致性非鎖定讀“。
- 對於帶鎖的select讀取,或者update tb set a = a+1(讀取a的當前值),稱為“當前讀”,也叫“一致性鎖定讀”。
如果在update、insert的時候,不能進行select,那麼服務的並發訪問性能就太差了。因此,我們日常的查詢,都是“快照讀”,不會上鎖,只有在updateinsert“當前讀”的時候,才會上鎖。而為了解決“快照讀”的並發訪問問題,就引入了MVCC。
2.2 多版本並發控制MVCC
如果說上面的行鎖是一種悲觀鎖,那麼MVCC就是一種樂觀鎖的實現方式,而且是一種很常用的樂觀鎖實現方式。
所謂多版本,就是一行記錄在數據庫中存儲了多個版本,每個版本以事務ID作為版本號。InnoDB 裏面每個事務有一個唯一的事務 ID,是在事務開始的時候向InnoDB的事務系統申請的,並且按照申請順序嚴格遞增的。假如一行記錄被多個事務更新,那麼,就會產生多個版本的記錄。
以某一行數據作為例子:
經過兩次事務的操作,value從22變成了19,同時,保留了三個事務id,15、25、30。
在每個記錄多版本的基礎上,需要利用“一致性視圖”,來做版本的可見性判斷。
這裡,我們要區分MySQL裏面的兩個”視圖”概念:
- 一個是view,通過語法create view … 實現,主要創建一個虛擬表,用來執行查詢語句。
- 一個是InnoDB用來實現mvcc的一致性視圖(consistent read view),純邏輯概念,沒有物理結構,定義了在事務期間,你能看到哪些版本的數據。
我們全文提到的“視圖”都是第二種,主要是支持InnoDB在“讀已提交”和“可重複讀”級別的並發訪問問題。
- “讀未提及”級別下,沒有一致性視圖
- “讀已提交”級別下,會在 每個SQL開始執行的時候 創建一致性視圖
- “可重複讀”級別下,會在 每個事務開始的時候 創建一致性視圖
- “串行化”級別下,直接通過加鎖避免並發問題
下面,我們簡單介紹一下創建一致性視圖的邏輯。
以“可重複讀”級別為例。
- 當一個事務開啟的時候,會向系統申請一個新事務id
- 此時,可能還有多個正在進行的其他事務沒有提交,因此在瞬時時刻,是有多個活躍的未提交事務id
- 將這些未提交的事務id組成一個數組,數組裏面最小的事務id記錄為低水位,當前系統創建過的事務id的最大值+1記錄為高水位
- 這個數組array 和 高水位,就組成了“一致性視圖”。
有了一致性視圖後,我們就可以判斷一行數據的多版本可見性了,無論是“讀已提交”還是“可重複讀”級別,可見性判斷規則是一樣的,區別在於創建快照(一致性視圖)的時間。
在當前事務中,讀取其他某一行的記錄,對其中的版本號的可見性判斷有五種情況(建議自己跟着捋一捋,挺重要的):
- 如果版本號小於“低水位”,說明事務已經提交,那肯定 可見;
- 如果版本號大於“高水位”,說明這行數據的這個事務id版本是在快照後產生的,那肯定 不可見;
- 如果版本號在事務數組array中,說明這個事務還沒提交,所以 不可見;
- 如果版本號不在事務數組array中,且低於高水位,說明這個事務已經提交,所以 可見;
- 當然,無論什麼時候,自己的事務id中的任何變化,都是可見的
可以看看下面這個例子,更容易理解。
系統創建過的事務id:1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
事務A啟動,拍個快照
此時未提交的事務id有:7,8,9
一致性視圖:數組array[7,8,9] + 高水位16(15+1)
對於任意一行數據的可見性判斷如下:
1)小於7的,可見
2)大於16的,說明是快照後產生的,不可見
3)10-15,不在數組array中,說明已經提交了,可見
4)7,8,9在array中,說明未提交,不可見
兩個重要結論:
- InnoDB 利用了“所有數據都有多個版本”的這個特性,實現了“秒級創建快照”的能力。
- MVCC的實現,就是根據當前事務的事務id為依據創建“一致性視圖”,利用一致性視圖來判斷數據版本的可見性。
3.隔離性實戰
下面,我們來兩個實戰案例,將上面的基礎概念與實現融會貫通吧。
1)並發select&update 案例
id=1 的value初始為1。
我們看下,在不同隔離級別,Time5、Time7、Time9事務A查詢到的value 分佈為多少。
- “讀未提交”:2,2,2
- “讀以提交”:1,2,2
- “可重複讀”:1,1,2
- 串行化:1,1,2(注意,這裡在事務A提交前,事務B都會阻塞,直到事務A提交後才能執行)
2)並發update案例
id=1 的value初始為1,在可重複讀級別:
我們看一下,你猜猜事務A和事務B讀取的value是多少?
答案是:1 和 3
可能會產生困惑,事務A在啟動後快照,所以讀到了1是正常的,但是事務2在啟動的時候快照了,然後在自己的事務中+1,怎麼會讀到3而不是2呢?
原因很簡單,即使是在可重複讀的級別,事務 更新數據 的時候,只能用當前讀(想想也能理解,不然update就出現數據不一致了)。
如果當前的記錄的行鎖被其他事務佔用的話,就需要進入鎖等待。而讀提交的邏輯和可重複讀的邏輯類似,它們最主要的區別是:在可重複讀隔離級別下,只需要在事務開始的時候創建一致性視圖,之後事務里的其他查詢都共用這個一致性視圖;在讀提交隔離級別下,每一個語句執行前都會重新算出一個新的視圖。
這裡,我們需要注意的是事務的啟動時機。
- begin/start transaction 命令並不是一個事務的起點,在執行到它們之後的第一個操作 InnoDB 表的語句,事務才真正啟動,一致性視圖是在執行第一個快照讀語句時創建的。
- 如果你想要馬上啟動一個事務,可以使用 start transaction with consistent snapshot 這個命令,一致性視圖是在執行 start transaction with consistent snapshot 時創建的。
4.關於幻讀
首先明確一下,什麼是幻讀?開篇介紹了什麼是幻讀,這裡再申明一下幻讀出現的場景
- 第一:事務的隔離級別為可重複讀,且是當前讀
- 第二:幻讀僅專指新插入的行,在範圍查詢中,後一次查詢出現了新的數據行。
前文已經提到了,對於普通數據庫,需要到可串行化的隔離級別才能解決幻讀問題。
而對於InnoDB存儲引擎來說,在可重複讀級別下就能解決幻讀問題。
InnoDB存儲引擎有三種行鎖算法:
- 行鎖:當個行記錄上的鎖
- 間隙鎖:Gap Lock,鎖定一個範圍,但不包含記錄本身
- Next-Key Lock:就是行鎖+間隙鎖,同時鎖上一個範圍,並且鎖定記錄本身
InnoDB就是通過Next-Key Lock解決了幻讀的問題,具體內容可以看我之前的文章: 前阿里數據庫專家總結的MySQL里的各種鎖(下篇)
看到這裡了,原創不易,點個關注、點個贊吧,你最好看了~
知識碎片重新梳理,構建Java知識圖譜:https://github.com/saigu/JavaKnowledgeGraph(歷史文章查閱非常方便)
掃碼關注我的公眾號“阿丸筆記”,第一時間獲取最新更新。同時可以免費獲取海量Java技術棧電子書、各個大廠面試題。