MySQL的併發控制與加鎖分析

  本文主要是針對MySQL/InnoDB的併發控制和加鎖技術作一個比較深刻的剖析,而且對其中涉及到的重要的概念,如多版本併發控制(MVCC),髒讀(dirty read),幻讀(phantom read),四種隔離級別(isolation level)等做詳細的闡述,而且基於一個簡單的例子,對MySQL的加鎖進行了一個詳細的分析。本文的總結參考了何登成前輩的博客,而且在前輩總結的基礎上,進行了一些基礎性的說明,但願對剛入門的同窗產生些許幫助,若有錯誤,請不吝賜教。按照個人寫做習慣,仍是經過幾個關鍵問題來組織行文邏輯,以下:html

  • 什麼是MVCC(多版本併發控制)?如何理解快照讀(snapshot read)和當前讀(current read)?
  • 什麼是隔離級別?髒讀?幻讀?InnoDB的四種隔離級別的含義是什麼?
  • 什麼是死鎖?
  • InnoDB是如何實現MVCC的?
  • 一個簡單的sql在不一樣場景下的加鎖分析
  • 一個複雜的sql的加鎖分析

  接下來,我將按照這幾個關鍵問題的順序,對以上問題做一一解答,而且在解答的過程當中,爭取將加鎖技術的細節,闡述的更加清楚。sql

1.1 MVCC:Multi-Version Concurrent Control 多版本併發控制數據庫

  MVCC是爲了實現數據庫的併發控制而設計的一種協議。從咱們的直觀理解上來看,要實現數據庫的併發訪問控制,最簡單的作法就是加鎖訪問,即讀的時候不能寫(容許多個西線程同時讀,即共享鎖,S鎖),寫的時候不能讀(一次最多隻能有一個線程對同一份數據進行寫操做,即排它鎖,X鎖)。這樣的加鎖訪問,其實並不算是真正的併發,或者說它只能實現併發的讀,由於它最終實現的是讀寫串行化,這樣就大大下降了數據庫的讀寫性能。加鎖訪問其實就是和MVCC相對的LBCC,即基於鎖的併發控制(Lock-Based Concurrent Control),是四種隔離級別中級別最高的Serialize隔離級別。爲了提出比LBCC更優越的併發性能方法,MVCC便應運而生。數據結構

  幾乎全部的RDBMS都支持MVCC。它的最大好處即是,讀不加鎖,讀寫不衝突。在MVCC中,讀操做能夠分紅兩類,快照讀(Snapshot read)和當前讀(current read)。快照讀,讀取的是記錄的可見版本(多是歷史版本,即最新的數據可能正在被當前執行的事務併發修改),不會對返回的記錄加鎖;而當前讀,讀取的是記錄的最新版本,而且會對返回的記錄加鎖,保證其餘事務不會併發修改這條記錄。在MySQL InnoDB中,簡單的select操做,如 select * from table where ? 都屬於快照讀;屬於當前讀的包含如下操做:併發

  1. select * from table where ? lock in share mode; (加S鎖)
  2. select * from table where ? for update; (加X鎖,下同)
  3. insert, update, delete操做

   針對一條當前讀的SQL語句,InnoDB與MySQL Server的交互,是一條一條進行的,所以,加鎖也是一條一條進行的。先對一條知足條件的記錄加鎖,返回給MySQL Server,作一些DML操做;而後再讀取下一條加鎖,直至讀取完畢。須要注意的是,以上須要加X鎖的都是當前讀,而普通的select(除了for update)都是快照讀,每次insert、update、delete以前都是會進行一次當前讀的,這個時候會上鎖,防止其餘事務對某些行數據的修改,從而形成數據的不一致性。咱們廣義上說的幻讀現象是經過MVCC解決的,意思是經過MVCC的快照讀可使得事務返回相同的數據集。以下圖所示:app

  

注意,咱們通常說在MyISAM中使用表鎖,由於MyISAM在修改數據記錄的時候會將整個表鎖起來;而InnoDB使用的是行鎖,即咱們以上所談的MVCC的加鎖問題。可是,並非InnoDB引擎不會使用表鎖,好比在alter table的時候,Innodb就會將該表用表鎖鎖起來。性能

1.2 隔離級別學習

  在SQL的標準中,定義了四種隔離級別。每一種級別都規定了,在一個事務中所作的修改,哪些在事務內和事務間是可見的,哪些是不可見的。低級別的隔離能夠執行更高級別的併發,性能好,可是會出現髒讀和幻讀的現象。首先,咱們從兩個基礎的概念提及:優化

  髒讀(dirty read):兩個事務,一個事務讀取到了另外一個事務未提交的數據,這即是髒讀。spa

  幻讀(phantom read):兩個事務,事務A與事務B,事務A在本身執行的過程當中,執行了兩次相同查詢,第一次查詢事務B未提交,第二次查詢事務B已提交,從而形成兩次查詢結果不同,這個其實被稱爲不可重複讀;若是事務B是一個會影響查詢結果的insert操做,則好像新多出來的行像幻覺同樣,所以被稱爲幻讀。其餘事務的提交會影響在同一個事務中的重複查詢結果。

  下面簡單描述一下SQL中定義的四種標準隔離級別:

  1. READ UNCOMMITTED (未提交讀) :隔離級別:0. 能夠讀取未提交的記錄。會出現髒讀。
  2. READ COMMITTED (提交讀) :隔離級別:1. 事務中只能看到已提交的修改。不可重複讀,會出現幻讀。(在InnoDB中,會加行所,可是不會加間隙鎖)該隔離級別是大多數數據庫系統的默認隔離級別,可是MySQL的則是RR。
  3. REPEATABLE READ (可重複讀) :隔離級別:2. 在InnoDB中是這樣的:RR隔離級別保證對讀取到的記錄加鎖 (記錄鎖),同時保證對讀取的範圍加鎖,新的知足查詢條件的記錄不可以插入 (間隙鎖),所以不存在幻讀現象。可是標準的RR只能保證在同一事務中屢次讀取一樣記錄的結果是一致的,而沒法解決幻讀問題。InnoDB的幻讀解決是依靠MVCC的實現機制作到的。
  4. SERIALIZABLE (可串行化):隔離級別:3. 該隔離級別會在讀取的每一行數據上都加上鎖,退化爲基於鎖的併發控制,即LBCC。

   須要注意的是,MVCC只在RC和RR兩個隔離級別下工做,其餘兩個隔離級別都和MVCC不兼容。

1.3 死鎖

  死鎖是指兩個或者多個事務在同一資源上相互做用,並請求鎖定對方佔用的資源,從而致使惡性循環的現象。當多個事務試圖以不一樣的順序鎖定資源時,就可能產生死鎖。多個事務同時鎖定同一個資源時,也會產生死鎖。且看下面的兩個產生死鎖的例子:

 

   第一個死鎖很好理解,而第二個死鎖,因爲在主索引(聚簇索引表)上仍舊是對兩條記錄進行了不一樣順序的加鎖,所以仍舊會形成死鎖。死鎖的發生與否,並不在於事務中有多少條SQL語句,死鎖的關鍵在於:兩個(或以上)的Session加鎖的順序不一致。所以,咱們經過分析加鎖細節,能夠判斷所寫的sql是否會發生死鎖,同時發生死鎖的時候,咱們應該如何處理。

1.4 InnoDB的MVCC實現機制

  MVCC能夠認爲是行級鎖的一個變種,它能夠在不少狀況下避免加鎖操做,所以開銷更低。MVCC的實現大都都實現了非阻塞的讀操做,寫操做也只鎖定必要的行。InnoDB的MVCC實現,是經過保存數據在某個時間點的快照來實現的。一個事務,無論其執行多長時間,其內部看到的數據是一致的。也就是事務在執行的過程當中不會相互影響。下面咱們簡述一下MVCC在InnoDB中的實現。

  InnoDB的MVCC,經過在每行記錄後面保存兩個隱藏的列來實現:一個保存了行的建立時間,一個保存行的過時時間(刪除時間),固然,這裏的時間並非時間戳,而是系統版本號,每開始一個新的事務,系統版本號就會遞增。在RR隔離級別下,MVCC的操做以下:

  1. select操做。a. InnoDB只查找版本早於(包含等於)當前事務版本的數據行。能夠確保事務讀取的行,要麼是事務開始前就已存在,或者事務自身插入或修改的記錄。b. 行的刪除版本要麼未定義,要麼大於當前事務版本號。能夠確保事務讀取的行,在事務開始以前未刪除。
  2. insert操做。將新插入的行保存當前版本號爲行版本號。
  3. delete操做。將刪除的行保存當前版本號爲刪除標識。
  4. update操做。變爲insert和delete操做的組合,insert的行保存當前版本號爲行版本號,delete則保存當前版本號到原來的行做爲刪除標識。

  因爲舊數據並不真正的刪除,因此必須對這些數據進行清理,innodb會開啓一個後臺線程執行清理工做,具體的規則是將刪除版本號小於當前系統版本的行刪除,這個過程叫作purge。

1.5 一個簡單SQL的加鎖分析

  在MySQL的InnoDB中,都是基於聚簇索引表的。並且普通的select操做都是基於快照讀,是不須要加鎖的。那麼咱們在分析其餘的sql語句的時候,如何分析加鎖細節?下面咱們以一個簡單的delete操做的SQL爲例,進行一個詳細的闡述。且看下面的SQL:

  delete from t1 where id=10;

  若是對這條SQL進行加鎖分析,那麼MySQL是如何加鎖的呢?通常狀況下,咱們直觀的感覺是:會在id=10的記錄上加鎖。可是,這樣輕率的下結論是片面的,要想肯定MySQL的加鎖狀況,咱們還須要知道更多的條件。還須要知道哪些條件呢?好比:

  1. id列是否是主鍵?
  2. 系統的隔離級別是什麼?
  3. id非主鍵的話,其上有創建索引嗎?
  4. 創建的索引是惟一索引嗎?
  5. 該SQL的執行計劃是什麼?索引掃描?全表掃描?

  接下來,我將這些問題的答案進行組合,而後按照從易到難的順序,逐個分析每種組合下,對應的SQL會加哪些鎖。

  • 組合1:id列是主鍵,RC隔離級別
  • 組合2:id列是二級惟一索引,RC隔離級別
  • 組合3:id列是二級非惟一索引,RC隔離級別
  • 組合4:id列上沒有索引,RC隔離級別
  • 組合5:id列是主鍵,RR隔離級別
  • 組合6:id列是二級惟一索引,RR隔離級別
  • 組合7:id列是二級非惟一索引,RR隔離級別
  • 組合8:id列上沒有索引,RR隔離級別
  • 組合9:Serializable隔離級別

  組合1:id列是主鍵,RC隔離級別

  當id是主鍵的時候,咱們只須要在該id=10的記錄上加上x鎖便可。以下圖所示:

  組合2:id列是二級惟一索引,RC隔離級別

  在這裏我先解釋一下聚簇索引和普通索引的區別。在InnoDB中,主鍵能夠被理解爲聚簇索引,聚簇索引中的葉子結點就是相應的數據行,具備聚簇索引的表也被稱爲聚簇索引表,數據在存儲的時候,是按照主鍵進行排序存儲的。咱們都知道,數據庫在select的時候,會選擇索引列進行查找,索引列都是按照B+樹(多叉搜索樹)數據結構進行存儲,找到主鍵以後,再回到聚簇索引表中進行查詢,這叫回表查詢。那咱們天然會問,當使用索引進行查詢的時候,與索引相對應的記錄會被上鎖嗎?會的。若是id是惟一索引,那麼只給該惟一索引所對應的索引記錄上x鎖;若是id是非惟一索引,那麼所對應的全部的索引記錄上都會上x鎖。以下圖所示:

  組合3:id列是二級非惟一索引,RC隔離級別

  解釋同上,以下圖:

  組合4:id列上沒有索引,RC隔離級別

    因爲id列上沒有索引,所以只能走聚簇索引,進行所有掃描。有人說會在表上加X鎖;有人說會在聚簇索引上,選擇出來的id = 10 的記錄加上X鎖。真實狀況以下圖:

  

  若id列上沒有索引,SQL會走聚簇索引的全掃描進行過濾,因爲過濾是由MySQL Server層面進行的。所以每條記錄,不管是否知足條件,都會被加上X鎖。可是,爲了效率考量,MySQL作了優化,對於不知足條件的記錄,會在判斷後放鎖,最終持有的,是知足條件的記錄上的鎖,可是不知足條件的記錄上的加鎖/放鎖動做不會省略。同時,優化也違背了2PL的約束(同時加鎖同時放鎖)。

  組合5,6同以上(由於只有一條結果記錄,只能在上面加鎖)

  組合7:id列是二級非惟一索引,RR隔離級別

   在RR隔離級別下,爲了防止幻讀的發生,會使用Gap鎖。這裏,你能夠把Gap鎖理解爲,不容許在數據記錄前面插入數據。首先,經過id索引定位到第一條知足查詢條件的記錄,加記錄上的X鎖,加GAP上的GAP鎖,而後加主鍵聚簇索引上的記錄X鎖,而後返回;而後讀取下一條,重複進行。直至進行到第一條不知足條件的記錄[11,f],此時,不須要加記錄X鎖,可是仍舊須要加GAP鎖,最後返回結束。以下圖所示:

  

 

  組合8:id列無索引,RR隔離級別

  在這種狀況下,聚簇索引上的全部記錄,都被加上了X鎖。其次,聚簇索引每條記錄間的間隙(GAP),也同時被加上了GAP鎖。以下圖:

  

  可是,MySQL是作了相關的優化的,就是所謂的semi-consistent read。semi-consistent read開啓的狀況下,對於不知足查詢條件的記錄,MySQL會提早放鎖,同時也不會添加Gap鎖。

  組合9:Serializable隔離級別

  和RR隔離級別同樣。

1.6 一個複雜的SQL的加鎖分析

  這裏咱們只是列出一個結論,由於要涉及到MySQL的where查詢條件的分析,所以這裏先不作詳細介紹,我會在以後的博客中詳細說明。以下圖:

 

  結論:在RR隔離級別下,針對一個複雜的SQL,首先須要提取其where條件。Index Key肯定的範圍,須要加上GAP鎖;Index Filter過濾條件,視MySQL版本是否支持ICP,若支持ICP,則不知足Index Filter的記錄,不加X鎖,不然須要X鎖;Table Filter過濾條件,不管是否知足,都須要加X鎖。加鎖的結果以下所示:

 

總結

本文只是對MVCC的一些基礎性的知識點進行了詳細的總結,參考了網上和書上比較多的資料和實例。但願能對各位的學習有所幫助。

相關文章
相關標籤/搜索