“本文略長,但值得認真研讀兩遍!!!“
在如今網際網路業務中使用範圍最廣的資料庫無疑還是關系型資料庫MySQL,之是以用"還是"這個詞,是因為最近幾年國内資料庫領域也取得了一些長足進步,例如以TIDB、OceanBase等為代表的分布式資料庫,但它們暫時還沒有形成絕對的覆寫面,是以現階段還得繼續學習MySQL資料庫以應對工作中遇到的一些問題,以及面試過程中關于資料庫部分的考察。
今天的内容就和大家聊一聊MySQL資料庫中關于并發控制、事務以及存儲引擎這幾個最核心的問題。本内容涉及的知識圖譜如下圖所示:

并發控制
并發控制是一個内容龐大的話題,在計算機軟體系統中隻要在同一時刻存在多個請求同時修改資料的情況,就都會産生并發控制的問題,例如Java中的多線程安全問題等。在MySQL中的并發控制,主要是讨論資料庫如何控制表資料的并發讀寫。
例如有一張表useraccount,其結構如下:
此時如果有如下兩條SQL語句同一時刻向資料庫發起請求:
SQL-A:
update useraccount t set t.account=t.account+100 where username='wudimanong';
複制
SQL-B:
update useraccount t set t.account=t.account-100 where username='wudimanong'
複制
當上述語句都執行完成,正确結果應該是account=100,但在并發情況下,卻有可能發生這樣的情況:
那麼在MySQL中是如何進行并發控制的呢?實際上與大多數并發控制方式一樣,在MySQL中也是利用鎖機制來實作并發控制的。
01 MySQL鎖類型
在MySQL中主要是通過"讀寫鎖"來實作并發控制。
讀鎖(read lock):也叫共享鎖(share lock),多個讀請求可以同時共享一把鎖來讀取資料,而不會造成阻塞。
寫鎖(write lock):也叫排他鎖(exclusive lock),寫鎖會排斥其他所有擷取鎖的請求,一直阻塞,直到完成寫入并釋放鎖。
讀寫鎖可以做到讀讀并行,但是無法做到寫讀、寫寫并行。後面會講到的事務隔離性就是根據讀寫鎖來實作的!
02 MySQL鎖粒度
上面提及的讀寫鎖是根據MySQL的鎖類型來劃分的,而讀寫鎖能夠施加的粒度在資料庫中主要展現為表和行,也稱為表鎖(table lock)、行鎖(row lock)。
表鎖(table lock):是MySQL中最基本的鎖政策,它會鎖定整張表,這樣維護鎖的開銷最小,但是會降低表的讀寫效率。如果一個使用者通過表鎖來實作對表的寫操作(插入、删除、更新),那麼先需要獲得鎖定該表的寫鎖,那麼在這種情況下,其他使用者對該表的讀寫都會被阻塞。一般情況下"alter table"之類的語句才會使用表鎖。
行鎖(row lock):行鎖可以最大程度地支援并發讀寫,但資料庫維護鎖的開銷會比較大。行鎖是我們日常使用最多的鎖政策,一般情況下MySQL中的行級鎖由具體的存儲引擎實作,而不是MySQL伺服器層面去實作(表鎖MySQL伺服器層面會實作)。
03 多版本并發控制(MVCC)
MVCC(MultiVersion Concurrency Control),多版本并發控制。在MySQL的大多數事務引擎(如InnoDB)中,都不隻是簡單地實作了行級鎖,否則會出現這樣的情況:"資料A被某個使用者更新期間(擷取行級寫鎖),其他使用者讀取該條資料(擷取讀鎖)都會被阻塞“。但現實情況顯然不是這樣,這是因為MySQL的存儲引擎基于提升并發性能的考慮,通過MVCC資料多版本控制,做到了讀寫分離,進而實作不加鎖讀取資料進而做到了讀寫并行。
以InnoDB存儲引擎的MVCC實作為例:
InnoDB的MVCC,是通過在每行記錄後面儲存兩個隐藏的列來實作的。這兩個列,一個儲存了行的建立時間,一個儲存了行的過期時間。當然它們存儲的并不是實際的時間值,而是系統版本号。每開啟一個新的事務,系統版本号都會自動遞增;事務開始時刻的系統版本号會作為事務的版本号,用來和查詢到的每行記錄的版本号進行比較。
MVCC在MySQL中實作所依賴的手段主要是:"undo log和read view"。
- undo log :undo log 用于記錄某行資料的多個版本的資料。
- read view :用來判斷目前版本資料的可見性
undo log在後面講述事務還會介紹到。關于MVCC的讀寫原理示意圖如下:
上圖示範了MySQL InnoDB存儲引擎,在REPEATABLE READ(可重複讀)事務隔離級别下,通過額外儲存兩個系統版本号(行建立版本号、行删除版本号)實作MVCC,進而使得大多數讀操作都可以不用再加讀鎖。這樣的設計使得資料讀取操作更加簡單、性能更好。
那麼在MVCC模式下資料讀取操作是如何保證資料讀取正确的呢?以InnoDB為例,Select時會根據以下兩個條件檢查每行記錄:
- 隻查找版本号小于或等于目前事務版本的資料行,這樣可以確定事務讀取的行要麼是在事務開始前已經存在,要麼是事務自身插入或者修過的。
- 行的删除版本号要麼未定義,要麼大于目前事務版本号。這樣可以確定事務讀取到的行,在事務開始之前未被删除。
隻有符合上述兩個條件的記錄,才能傳回作為查詢的結果!以圖中示範的邏輯為例,寫請求将account變更為200的過程中,InnoDB會再插入一行新記錄(account=200),并将目前系統版本号作為行建立版本号(createVersion=2),同時将目前系統版本号作為原來行的行删除版本号(deleteVersion=2),那麼此時關于這條資料有兩個版本的資料副本,具體如下:
假如現在寫操作還未結束,事務對其他使用者暫不可見,按照Select檢查條件隻有accout=100的記錄才符合條件,是以查詢結果會傳回account=100的記錄!
上述過程就是InnoDB存儲引擎關于MVCC實作的基本原理,但是後面需要注意MVCC多版本并發控制的邏輯隻能工作在“REPEATABLE READ(可重複讀)和READ COMMITED(送出讀)”兩種事務隔離級别下。其他兩個隔離級别都與MVCC不相容,因為READ UNCOMMITED(未送出讀)總是讀取最新的資料行,而不是符合目前事務版本的資料行;而SERIALIZABLE則會對所有讀取的行都加鎖,也不符合MVCC的思想。
MySQL事務
前面在講解了關于MySQL并發控制的過程中,也提到了事務相關的内容,接下來我們來更全面的梳理下關于事務的核心知識。
相信大家在日常的開發過程中,都使用過資料庫事務,對事務的特點也都能張口就來——ACID。那麼事務内部到底是怎麼實作的呢?在接下來的内容中,就來和大家具體聊一聊這個問題!
01 事務概述
資料庫事務本身所要達成的效果主要展現在:"可靠性"以及"并發處理"這兩個方面。
- 可靠性:資料庫要保證當insert或update操作抛出異常,或者資料庫crash的時候要保障資料操作的前後一緻。
- 并發處理:說的是當多個并發請求過來,并且其中有一個請求是對資料進行修改操作,為了避免其他請求讀到髒資料,需要對事務之間的讀寫進行隔離。
實作MySQL資料庫事務功能主要有三個技術,分别是日志檔案(redo log和undo log)、鎖技術及MVCC。
02 redo log與undo log
redo log與undo log是實作MySQL事務功能的核心技術。
1)、redo log
redo log叫做重做日志,是實作事務持久性的關鍵。redo log日志檔案主要由2部分組成:重做日志緩沖(redo log buffer)、重做日志檔案(redo log file)。
在MySql中為了提升資料庫性能并不會把每次的修改都實時同步到磁盤,而是會先存到一個叫做“Boffer Pool”的緩沖池中,之後會再使用背景線程去實作緩沖池和磁盤之間的同步。
如果采取這樣的模式,可能會出現這樣的問題:如果在資料還沒來得及同步的情況下出現當機或斷電,那麼就可能會丢失部分已送出事務的修改資訊!而這種情況對于資料庫軟體來說是不可以接受的。
是以redo log的主要作用就是用來記錄已成功送出事務的修改資訊,并且會在事務送出後實時将redo log持久化到磁盤,這樣在系統重新開機之後就可以讀取redo log來恢複最新的資料。
接下來我們以前面SQL-A所開啟的事務為例來示範redo log的具體是如何運作的,如下圖所示:
如上圖所示,當修改一行記錄的事務開啟,MySQL存儲引擎是把資料從磁盤讀取到記憶體的緩沖池上進行修改,這個時候資料在記憶體中被修改後就與磁盤中的資料産生了差異,這種有差異的資料也被稱之為“髒頁”。
而一般存儲引擎對于髒頁的處理并不是每次生成髒頁就即刻将髒頁重新整理回磁盤,而是通過背景線程“master thread”以大緻每秒運作一次或每10秒運作一次的頻率去重新整理磁盤。在這種情況下,出現資料庫當機或斷電等情況,那麼尚未重新整理回磁盤的資料就有可能丢失。
而redo log日志的作用就是為了調和記憶體與磁盤的速度差異。當事務被送出時,存儲引擎會首先将要修改的資料寫入redo log,然後再去修改緩沖池中真正的資料頁,并實時重新整理一次資料同步。如果在這個過程中,資料庫挂了,由于redo log實體日志檔案已經記錄了事務修改,是以在資料庫重新開機後就可以根據redo log日志進行事務資料恢複。
2)、undo log
上面我們聊了redo log日志,它的作用主要是用來恢複資料,保障已送出事務的持久化特性。在MySQL中還有另外一種非常重要的日志類型undo log,又叫復原日志,它主要是用于記錄資料被修改前的資訊,這與記錄資料被修改後資訊的redo log日志正好相反。
undo log 主要記錄事務修改之前版本的資料資訊,假如由于系統錯誤或者rollback操作而復原的話就可以根據undo log日志來将資料復原到沒被修改之前的狀态。
每次寫入資料或者修改資料之前存儲引擎都會将修改前的資訊記錄到undo log。
03 事務的實作
前面我們講到了鎖、多版本并發控制(MVCC)、重做日志(redo log)以及復原日志(undo log),這些内容就是MySQL實作資料庫事務的基礎。從事務的四大特性來說,其對應關系主要展現如下:
實際上事務原子性、持久性、隔離性的最終目的都是為了確定事務資料的一緻性。而ACID隻是個概念,事務的最終目的是要保障資料的可靠性和一緻性。
接下來我們再具體分析下事務ACID特性的實作原理。
1)、原子性的實作
原子性,是指一個事務必須被視為不可分割的最小機關,一個事務中的所有操作要麼全部執行成功、要麼全部失敗復原,對一個事務來說不可能隻執行其中的部分操作,這就是事務原子性的概念。
而MySQL資料庫實作原子性的主要是通過復原操作來實作的。所謂復原操作就是當發生錯誤異常或者顯示地執行rollback語句時需要把資料還原到原先的模樣,而這個過程就需要借助undo log來進行。具體規則如下:
- 每條資料變更(insert/update/delete)操作都伴随着一條undo log的生成,并且復原日志必須先于資料持久化到磁盤上;
- 所謂的復原就是根據undo log日志做逆向操作,比如delete的逆向操作為insert,insert的逆向操作為delete,update的逆向操作為update等;
2)、持久性的實作
持久性,指的是事務一旦送出其所作的修改會永久地儲存到資料庫中,此時即使系統崩潰修改的資料也不會丢失。
事務的持久性主要是通過redo log日志來實作的。redo log日志之是以能夠彌補緩存同步所造成的資料差異,主要其具備以下特點:
- redo log的存儲是順序的,而緩存同步則是随機操作;
- 緩存同步是以資料頁為機關,每次傳輸的資料大小大于redo log;
關于redo log實作事務持久性的邏輯可參考本文前面關于redo log部分的内容!
3)、隔離性的實作
隔離性是事務ACID特性中最複雜的一個。在SQL标準裡定義了四種隔離級别,每一種隔離級别都規定一個事務中的修改,那些是事務之間可見的,那些是不可見的。
MySQL隔離級别有以下四種(級别由低到高):
- READ UNCOMMITED (未送出讀);
- READ COMMITED (送出讀)
- REPEATABLE READ (可重複讀)
- SERIALIZABLE (可串行化)
隔離級别越低,則資料庫可以執行的并發度越高,但是實作的複雜度和開銷也越大。隻要徹底了解了隔離級别以及它的實作原理,就相當于了解了ACID中的事務隔離性。
前面提到過,原子性、持久性、隔離性的目的最終都是為了實作資料的一緻性,但隔離性與其它兩個有所差別,原子性和持久性主要是為了保障資料的可靠性,比如做到當機後的資料恢複,以及錯誤後的資料復原。而隔離性的核心目标則是要管理多個并發讀寫請求的通路順序,實作資料庫資料的安全和高效通路,實質上就是一場資料的安全性與性能之間的權衡遊戲。
可靠性高的隔離級别,并發性能低(例如SERIALIZABLE隔離級别,因為所有的讀寫都會加鎖);而可靠性低的,并發性能高(例如READ UNCOMMITED,因為讀寫完全不加鎖)。
接下來我們再分别分析下這四種隔離級别的特點:
READ UNCOMMITTED
在READ UNCOMMITTED隔離級别下,一個事務中的修改即使還沒有送出,對其它事務也是可見,也就是說事務可以讀取到未送出的資料。
因為讀不會添加鎖,是以寫操作在讀的過程中修改資料的話會造成"髒讀"。未送出讀隔離級别讀寫示意圖如下:
如上圖所示,寫請求将account修改為200,此時事務未送出;但是讀請求可以讀取到未送出的事務資料account=200;随後寫請求事務失敗復原account=100;那麼此時讀請求讀取的account=200的資料就是髒資料。
這種隔離級别的優點是讀寫并行、性能高;但是缺點是容易造成髒讀。是以在MySQL資料庫中一般情況下并不會采取此種隔離級别!
READ COMMITED
這種事務隔離級别也叫"不可重複讀或送出讀"。它的特點是一個事務在它送出之前的所有修改,其它事務都是不可見的;其它事務隻能讀到已送出的修改變化。
這種隔離級别看起來很完美,也符合大部分邏輯場景,但該事務隔離級别會産生"不可重讀"和"幻讀"的問題。
不可重讀:是指一個事務内多次讀取的相同行的資料,結果卻不一樣。例如事務A讀取a行資料,而事務B此時修改了a行的資料并送出了事務,那麼事務A在下一次讀取a行資料時,發現和第一次不一樣了!
幻讀:是指一個事務按照相同的查詢條件檢索資料,但是多次檢索出的資料結果卻不一樣。例如事務A第一次以條件x=0檢索資料擷取了5條記錄;此時事務B向表中插入了一條x=0的資料并送出了事務;那麼事務A第二次再以條件x=0檢索資料時,發現擷取了6條記錄!
那麼在READ COMMITED隔離級别下為什麼會産生不可重複讀和幻讀的問題呢?
實際上不可重複讀事務隔離級别也采用了我們前面講過的MVCC(多版本并發控制)機制。但在READ COMMITED隔離級别下的MVCC機制,會在每次select的時候都生成一個新的系統版本号,是以事務中每次select操作讀到的不是一個副本而是不同的副本資料,是以在每次select之間,如果有其它事務更新并送出了我們讀取的資料,那麼就會産生不可重複讀和幻讀的現象。
不可重複讀産生的原因示意圖如下:
REPEATABLE READ
事務隔離級别REPEATABLE READ,也叫可重複讀,它是MySQL資料庫的預設事務隔離級别。在這種事務隔離級别下,一個事務内的多次讀取結果是一緻的,這種隔離級别可以避免髒讀、不可重複讀等查詢問題。
這種事務隔離級别的實作手段主要是采用讀寫鎖+MVCC機制。具體示意圖如下:
如上圖所示,在該事務隔離級别下的MVCC機制,并不會在事務内每次查詢都産生一個新的系統版本号,是以一個事務内的多次查詢,資料副本都是一個,是以不會産生不可重複讀問題。關于此隔離級别下MVCC更多的細節可參考前面内容!
但是需要注意,此隔離級别解決了不可重複讀的問題,但是并沒有解決幻讀的問題,是以如果事務A中存在條件查詢,另外一個事務B在此期間新增或删除了該條件的資料并送出了事務,那麼依然會造成事務A産生幻讀。是以在使用MySQL時需要注意這個問題!
SERIALIZABLE
該隔離級别了解起來最簡單,因為它讀寫請求都會加排他鎖,是以不會造成任何資料不一緻的問題,就是性能不高,是以采用此隔離級别的資料庫很少!
4)、一緻性的實作
一緻性主要是指通過復原、恢複以及在并發條件下的隔離性來實作資料庫資料的一緻!前面所講述的原子性、持久性及隔離性最終就是為了實作一緻性!
MySQL存儲引擎
前面的内容我們分别講述了MySQL并發控制和事務的内容,而實際上在并發控制和事務的具體細節都是依賴于MySql存儲引擎來實作的。MySQL最重要、最與衆不同的特性就是它的存儲引擎架構,這種将資料處理和存儲分離的架構設計使得使用者在使用時可以根據性能、特性以及其它具體需求來選擇相應的存儲引擎。
雖然如此,但絕大部分情況下使用MySQL資料庫時選擇的還是InnoDB存儲引擎,不過這并不妨礙我們适當地了解下其它存儲引擎的特點。接下來給大家簡單總結下,具體如下:
以上我們簡單總結了MySQL各種存儲引擎的大概特點及其大緻适用的場景,但實際上除了InnoDB存儲引擎外,在網際網路業務中很少會看到其它存儲引擎的身影。雖然MySQL内置了多種針對特定場景的存儲引擎,但是它們大多都有相應的替代技術,例如日志類應用現在有Elasticsearch、而數倉類應用現在則有Hive、HBase等産品,至于記憶體資料庫有MangoDB、Redis等NoSQL資料産品,是以能夠給MySQL發揮的也隻有InnoDB了!
PS:以上就是本文的全部内容希望能夠對你有所幫助!
—————END—————
參考文檔:
https://mp.weixin.qq.com/s/IvQWnts592KlDCnoXuP7DQ