1. 程式人生 > >【轉】【MySQL】MySQL的併發控制與加鎖分析

【轉】【MySQL】MySQL的併發控制與加鎖分析

https://www.cnblogs.com/yelbosh/p/5813865.html

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

  • 什麼是MVCC(多版本併發控制)?如何理解快照讀(snapshot read)和當前讀(current read)?
  • 什麼是隔離級別?髒讀?幻讀?InnoDB的四種隔離級別的含義是什麼?
  • 什麼是死鎖?
  • InnoDB是如何實現MVCC的?
  • 一個簡單的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的快照讀可以使得事務返回相同的資料集。如下圖所示:

  

注意,我們一般說在MyISAM中使用表鎖,因為MyISAM在修改資料記錄的時候會將整個表鎖起來;而InnoDB使用的是行鎖,即我們以上所談的MVCC的加鎖問題。但是,並不是InnoDB引擎不會使用表鎖,比如在alter table的時候,Innodb就會將該表用表鎖鎖起來。

1.2 隔離級別

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

  髒讀(dirty read):兩個事務,一個事務讀取到了另一個事務未提交的資料,這便是髒讀。

  幻讀(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的一些基礎性的知識點進行了詳細的總結,參考了網上和書上比較多的資料和例項。希望能對各位的學習有所幫助。