五分鐘詳解MySQL併發控制及事務原理

五分鐘詳解MySQL併發控制及事務原理

在現在互聯網業務中使用範圍最廣的數據庫無疑仍是關係型數據庫MySQL,之因此用"仍是"這個詞,是由於最近幾年國內數據庫領域也取得了一些長足進步,例如以TIDB、OceanBase等爲表明的分佈式數據庫,但它們暫時尚未造成絕對的覆蓋面,因此現階段還得繼續學習MySQL數據庫以應對工做中遇到的一些問題,以及面試過程當中關於數據庫部分的考察。面試

今天的內容就和你們聊一聊MySQL數據庫中關於併發控制、事務以及存儲引擎這幾個最核心的問題。本內容涉及的知識圖譜以下圖所示:數據庫

五分鐘詳解MySQL併發控制及事務原理

併發控制

併發控制是一個內容龐大的話題,在計算機軟件系統中只要在同一時刻存在多個請求同時修改數據的狀況,就都會產生併發控制的問題,例如Java中的多線程安全問題等。在MySQL中的併發控制,主要是討論數據庫如何控制表數據的併發讀寫。緩存

例若有一張表useraccount,其結構以下:安全

五分鐘詳解MySQL併發控制及事務原理

此時若是有以下兩條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中是如何進行併發控制的呢?實際上與大多數併發控制方式同樣,在MySQL中也是利用鎖機制來實現併發控制的。分佈式

1.MySQL鎖類型ide

在MySQL中主要是經過"讀寫鎖"來實現併發控制。

讀鎖(read lock):也叫共享鎖(share lock),多個讀請求能夠同時共享一把鎖來讀取數據,而不會形成阻塞。

寫鎖(write lock):也叫排他鎖(exclusive lock),寫鎖會排斥其餘全部獲取鎖的請求,一直阻塞,直到完成寫入並釋放鎖。

讀寫鎖能夠作到讀讀並行,可是沒法作到寫讀、寫寫並行。後面會講到的事務隔離性就是根據讀寫鎖來實現的!

2.MySQL鎖粒度

上面說起的讀寫鎖是根據MySQL的鎖類型來劃分的,而讀寫鎖可以施加的粒度在數據庫中主要體現爲表和行,也稱爲表鎖(table lock)、行鎖(row lock)

表鎖(table lock):是MySQL中最基本的鎖策略,它會鎖定整張表,這樣維護鎖的開銷最小,可是會下降表的讀寫效率。若是一個用戶經過表鎖來實現對錶的寫操做(插入、刪除、更新),那麼先須要得到鎖定該表的寫鎖,那麼在這種狀況下,其餘用戶對該表的讀寫都會被阻塞。通常狀況下"alter table"之類的語句纔會使用表鎖。

行鎖(row lock):行鎖能夠最大程度地支持併發讀寫,但數據庫維護鎖的開銷會比較大。行鎖是咱們平常使用最多的鎖策略,通常狀況下MySQL中的行級鎖由具體的存儲引擎實現,而不是MySQL服務器層面去實現(表鎖MySQL服務器層面會實現)。

3.多版本併發控制(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併發控制及事務原理

上圖演示了MySQL InnoDB存儲引擎,在REPEATABLE READ(可重複讀)事務隔離級別下,經過額外保存兩個系統版本號(行建立版本號、行刪除版本號)實現MVCC,從而使得大多數讀操做均可以不用再加讀鎖。這樣的設計使得數據讀取操做更加簡單、性能更好。

那麼在MVCC模式下數據讀取操做是如何保證數據讀取正確的呢?以InnoDB爲例,Select時會根據如下兩個條件檢查每行記錄:

  • 只查找版本號小於或等於當前事務版本的數據行,這樣能夠確保事務讀取的行要麼是在事務開始前已經存在,要麼是事務自身插入或者修過的。

  • 行的刪除版本號要麼未定義,要麼大於當前事務版本號。這樣能夠確保事務讀取到的行,在事務開始以前未被刪除。

只有符合上述兩個條件的記錄,才能返回做爲查詢的結果!以圖中示範的邏輯爲例,寫請求將account變動爲200的過程當中,InnoDB會再插入一行新記錄(account=200),並將當前系統版本號做爲行建立版本號(createVersion=2),同時將當前系統版本號做爲原來行的行刪除版本號(deleteVersion=2),那麼此時關於這條數據有兩個版本的數據副本,具體以下:

五分鐘詳解MySQL併發控制及事務原理

假如如今寫操做還未結束,事務對其餘用戶暫不可見,按照Select檢查條件只有accout=100的記錄才符合條件,所以查詢結果會返回account=100的記錄!

上述過程就是InnoDB存儲引擎關於MVCC實現的基本原理,可是後面須要注意MVCC多版本併發控制的邏輯只能工做在「REPEATABLE READ(可重複讀)和READ COMMITED(提交讀)」兩種事務隔離級別下。其餘兩個隔離級別都與MVCC不兼容,由於READ UNCOMMITED(未提交讀)老是讀取最新的數據行,而不是符合當前事務版本的數據行;而SERIALIZABLE則會對全部讀取的行都加鎖,也不符合MVCC的思想。

MySQL事務

前面在講解了關於MySQL併發控制的過程當中,也提到了事務相關的內容,接下來咱們來更全面的梳理下關於事務的核心知識。

相信你們在平常的開發過程當中,都使用過數據庫事務,對事務的特色也都能張口就來——ACID。那麼事務內部究竟是怎麼實現的呢?在接下來的內容中,就來和你們具體聊一聊這個問題!

1.事務概述

數據庫事務自己所要達成的效果主要體如今:"可靠性"以及"併發處理"這兩個方面。

  • 可靠性:數據庫要保證當insert或update操做拋出異常,或者數據庫crash的時候要保障數據操做的先後一致。

  • 併發處理:說的是當多個併發請求過來,而且其中有一個請求是對數據進行修改操做,爲了不其餘請求讀到髒數據,須要對事務之間的讀寫進行隔離。

實現MySQL數據庫事務功能主要有三個技術,分別是日誌文件(redo log和undo log)、鎖技術及MVCC。

2.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併發控制及事務原理

如上圖所示,當修改一行記錄的事務開啓,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。

3.事務的實現

前面咱們講到了鎖、多版本併發控制(MVCC)、重作日誌(redo log)以及回滾日誌(undo log),這些內容就是MySQL實現數據庫事務的基礎。從事務的四大特性來講,其對應關係主要體現以下:

五分鐘詳解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隔離級別下,一個事務中的修改即便尚未提交,對其它事務也是可見,也就是說事務能夠讀取到未提交的數據。

由於讀不會添加鎖,因此寫操做在讀的過程當中修改數據的話會形成"髒讀"。未提交讀隔離級別讀寫示意圖以下:

五分鐘詳解MySQL併發控制及事務原理

如上圖所示,寫請求將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之間,若是有其它事務更新並提交了咱們讀取的數據,那麼就會產生不可重複讀和幻讀的現象。

不可重複讀產生的緣由示意圖以下:

五分鐘詳解MySQL併發控制及事務原理

REPEATABLE READ

事務隔離級別REPEATABLE READ,也叫可重複讀,它是MySQL數據庫的默認事務隔離級別。在這種事務隔離級別下,一個事務內的屢次讀取結果是一致的,這種隔離級別能夠避免髒讀、不可重複讀等查詢問題。

這種事務隔離級別的實現手段主要是採用讀寫鎖+MVCC機制。具體示意圖以下:

五分鐘詳解MySQL併發控制及事務原理

如上圖所示,在該事務隔離級別下的MVCC機制,並不會在事務內每次查詢都產生一個新的系統版本號,因此一個事務內的屢次查詢,數據副本都是一個,所以不會產生不可重複讀問題。關於此隔離級別下MVCC更多的細節可參考前面內容!

可是須要注意,此隔離級別解決了不可重複讀的問題,可是並無解決幻讀的問題,因此若是事務A中存在條件查詢,另一個事務B在此期間新增或刪除了該條件的數據並提交了事務,那麼依然會形成事務A產生幻讀。因此在使用MySQL時須要注意這個問題!

SERIALIZABLE

該隔離級別理解起來最簡單,由於它讀寫請求都會加排他鎖,因此不會形成任何數據不一致的問題,就是性能不高,因此採用此隔離級別的數據庫不多!

4)、一致性的實現

一致性主要是指經過回滾、恢復以及在併發條件下的隔離性來實現數據庫數據的一致!前面所講述的原子性、持久性及隔離性最終就是爲了實現一致性!

MySQL存儲引擎

前面的內容咱們分別講述了MySQL併發控制和事務的內容,而實際上在併發控制和事務的具體細節都是依賴於MySql存儲引擎來實現的。MySQL最重要、最不同凡響的特性就是它的存儲引擎架構,這種將數據處理和存儲分離的架構設計使得用戶在使用時能夠根據性能、特性以及其它具體需求來選擇相應的存儲引擎。

雖然如此,但絕大部分狀況下使用MySQL數據庫時選擇的仍是InnoDB存儲引擎,不過這並不妨礙咱們適當地瞭解下其它存儲引擎的特色。接下來給你們簡單總結下,具體以下:

五分鐘詳解MySQL併發控制及事務原理

以上咱們簡單總結了MySQL各類存儲引擎的大概特色及其大體適用的場景,但實際上除了InnoDB存儲引擎外,在互聯網業務中不多會看到其它存儲引擎的身影。雖然MySQL內置了多種針對特定場景的存儲引擎,可是它們大多都有相應的替代技術,例如日誌類應用如今有Elasticsearch、而數倉類應用如今則有Hive、HBase等產品,至於內存數據庫有MangoDB、Redis等NoSQL數據產品,因此可以給MySQL發揮的也只有InnoDB了!

相關文章
相關標籤/搜索