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的加鎖情況,我們還需要知道更多的條件。還需要知道哪些條件呢?比如:

id列是不是主鍵?
系統的隔離級別是什麼?
id非主鍵的話,其上有建立索引嗎?
建立的索引是唯一索引嗎?
該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鎖。加鎖的結果如下所示:
在這裏插入圖片描述

發佈了20 篇原創文章 · 獲贊 3 · 訪問量 5833
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章