MySQL的鎖及其MVCC

摘要

  在當下互聯網技術的發展狀態下,數據的高併發是隨處可見,那麼數據庫如何解決高併發所帶來的問題呢。鎖便是計算機用於解決多進程或多線程併發服務中保持數據一致性的關鍵。本文主要介紹MySQL中的鎖。

  本人還寫了MySQL相關博文,有興趣的研友可以點擊如下鏈接,請各位研友指正並留言。
   MySQL索引及優化
   MySQL事務與隔離級別

一、樂觀鎖與悲觀鎖

  樂觀鎖與悲觀鎖並不是實際的鎖,它是對鎖的一種抽象。本文要講解的MySQL中的鎖都屬於悲觀鎖,MVCC機制則屬於樂觀鎖。

1.1 樂觀鎖

  樂觀鎖認爲本次服務對數據進行操作時,其他服務不會修改數據,所以樂觀鎖在操作數據時,並不會加鎖抵抗其他服務修改數據,而是在對數據操作完之後再去校驗是否有其他服務修改了數據。比如在數據中添加一列版本號,每次服務提取數據與版本號,完成服務時,版本號加一,當提交本次服務時,若本次服務版本號大於數據庫中的版本號則持久化,若本次服務版本號低於數據庫版本號則認爲其他服務已對數據進行了更新,應放棄本次服務的修改;Redis中的鎖就是一種樂觀鎖。本文最後講解的MVCC也是一種樂觀鎖。

1.2 悲觀鎖

  悲觀鎖認爲本次對數據進行操作時,其他服務會修改數據,所以悲觀鎖在讀取數據時對數據進行上鎖,其他服務只能等待其鎖釋放,從而達到了一種串行化執行順序,保證了數據的一致性,但由於服務進行串行執行,所以吞吐量慢,同時易造成死鎖。MySQL中的鎖,如共享讀和排他寫都屬於悲觀鎖。

二、MySQL鎖分類

  MySQL中的鎖按訪問粒度可分爲:

  1. 表鎖:鎖定一張表。
  2. 行鎖:鎖定一張表的某一頁的某一行。
  3. 頁面鎖:鎖定一張表的某一頁。

  本部分只講解常用的表鎖與行鎖,頁面鎖的效率間於表鎖與行鎖之間。

2.1 表鎖

  MySQL的存儲引擎MyISAM只支持表鎖,即對錶中的所有行進行限定訪問。當用戶只是更新一行數據時,也會造成其他行不可訪問,由此可見表鎖的粒度比較大,在高併發情況下,數據吞吐量很低,但數據的一致性得到最大化。由於訪問數據的服務由於表鎖需要排隊訪問,所以不會存在死鎖的問題。
  表鎖的特點:開銷小,加鎖快;不會出現死鎖;鎖定粒度大,併發度最低。
  表鎖分爲兩種:

  1. 共享讀鎖 :不會阻塞其他服務對同一表的讀請求,但會阻塞對同一表的寫請求;
  2. 排它寫鎖:會阻塞其他服務對同一表的讀和寫操作;

  由定義可知:若一個線程對數據加了共享讀鎖之後,其他線程可以繼續加共享讀鎖,但是其他線程想加排它寫鎖時,需要等待當前線程釋放共享讀鎖;若一個線程對數據加了排它寫鎖之後,不允許其他線程加任何鎖,其他線程只能等待當前線程釋放排它鎖之後才能獲取加鎖權限,若等待的線程中既有共享讀鎖或排它寫鎖,那麼MySQL釋放鎖之後,誰先獲取呢?MySQL中默認排它寫鎖的優先級高於共享讀鎖,所以默認情況下其他排它鎖會優先加鎖。
   這樣的表鎖機制會帶來什麼問題呢?在高併發的互聯網應用場景中,由於排它寫鎖優先級高於共享讀鎖,所以有可能導致共享讀書沒有機會執行。
  MyISAM存儲引擎支持併發插入,以減少給定表的讀和寫操作之間的爭用:如果MyISAM表在數據文件中間沒有空閒塊,則行始終插入數據文件的末尾, 在這種情況下,併發使用INSERT和SELECT語句是不需要加鎖的,即可以在其他線程進行讀操作的時候,同時將行插入到MyISAM表中。 當數據文件由於刪除或更新而產生空閒塊時,則併發插入機制會自動關閉,若數據空閒塊被填充後,併發插入機制又會開啓。
  MyISAM引擎中鎖的設置:
  1、共享讀鎖:

lock table xuebao read;

  2、排它寫鎖:

lock table xuebao write;

  3、釋放鎖:

unlock tables;

2.2 行鎖

  MySQL的InnoDB引擎支持行級鎖,但InnoDB的行鎖並不是真實的在一張表中對某一行進行加鎖,而是對索引的鍵進行加鎖,所以一條SQL在實際執行時,若沒有用到索引,則該SQL不會使用到行鎖而是升級爲表鎖。
  InnoDB引擎的鎖可以分爲如下幾類:
  1、意向共享鎖;
  2、意向排他鎖;
  3、共享鎖;
  4、排他鎖;
  5、間隙鎖;

2.2.1 意向共享鎖

  意向共享鎖是自動添加的,不需要用戶的干預。InnoDB事務在獲取共享鎖之前,需要先獲取到該數據的意向共享鎖。

2.2.2 意向排他鎖

  意向排他鎖是自動添加的,不需要用戶的干預。InnoDB事務在獲取排他鎖之前,需要先獲取到該數據的意向排他鎖。

2.2.3 共享鎖

  共享鎖允許多個事務的共享鎖去讀取一行數據,但會阻塞其他事務的排他鎖。
  MySQL的SELECT語句並沒有默認加共享鎖或排他鎖,所以SELECT加共享鎖如下所示:

SELECT id from student_table where age = 16 LOCAL IN SHARE MODE;

2.2.4 排他鎖

  排他鎖會阻塞其他任何鎖,處於獨佔數據的狀態。
  MySQL的UPDATE、DELETE 和 INSERT語句會自動添加排他鎖。SELECT語句也可以添加排他鎖,如下所示:

SELECT id from student_table where age = 16 FOR UPDATE;

需要注意的是沒有加任何鎖的SELECT語句,並不會與讀鎖或寫鎖產生鎖競爭關係,所以某一行已經加排他鎖了,但沒有加任何鎖的SELECT語句還是可以訪問到數據。

2.2.5 間隙鎖

  間隙鎖也是InnoDB引擎管理的,不需要用戶的干預。間隙鎖產生的原因需要從InnoDB產生鎖講起。因爲InnoDB引擎的鎖時針對索引實現的,而索引是一種數據結構,無論實現該數據結構的是B-Tree還是B+Tree,一個索引鍵值與另一個鍵值之間是用鏈表實現的,那麼我們在使用行鎖時,若指定的行不是具體某一行,而是一個範圍時,InnoDB實際鎖定的是對應的一個索引範圍,所以鏈表之間的指針也被鎖定了不準修改,其他事務想在該範圍內插入數據也就行不通了,這就形成了間隙鎖。如下所示:

SELECT * from student_table where id > 20 FOR UPDATE;

  id爲主鍵,id>20爲範圍條件,所以該條語句會自動添加間隙鎖。
  間隙鎖的缺點:從間隙鎖的鎖定粒度來看,要大於行鎖的鎖定粒度,那麼在高併發情景下,間隙鎖的數據吞吐量就會小於行鎖。所以用戶訪問數據時,儘量使用相等條件,少用範圍條件。

需要注意的是當對不存在的一行添加鎖時,InnoDB引擎同樣會使用間隙鎖,導致其他事務想插入該條不存在的行數據時被阻塞。

三、鎖的常見問題

  鎖的常見問題有兩種:
  1、行鎖升級爲表鎖
  2、死鎖
  3、鎖的性能優化

3.1 行鎖升級爲表鎖

  在介紹行鎖時,已經指出InnoDB的行鎖是基於索引實現的,如果訪問數據的語句實際沒有用到索引,那麼就不可能用到行鎖。如何知道訪問語句有沒有使用到索引呢?需要用戶使用explain對單條SQL進行排查,其方法詳見文獻《MySQL索引及優化》。當SQL語句使用不到索引而不用加行鎖時,InnoDB會自動將行鎖升級爲表鎖。
  避免措施:從上對行鎖升級爲表鎖的介紹可知,用戶需要加鎖的SQL語句一定要通過explain檢測是否使用了索引,從而可避免行鎖升級表鎖。

3.2 死鎖

  兩個事務A、B,都需要獲取兩個資源X、Y,A事務先獲取到X資源,B事務先獲取到Y資源,此時事務A、B並不衝突,現在A事務開始請求Y資源,因Y資源正被B事務加鎖佔用,從而A事務阻塞並等待B事務釋放Y資源,同時,B事務開始請求X資源,因X資源正被A事務加鎖佔用,從而B事務阻塞並等待A事務釋放X資源。我們把這種A、B事務同時阻塞並等待對方資源的狀態稱爲死鎖。

3.2.1 InnoDB處理死鎖的方式

  在數據發生死鎖之後,InnoDB引擎一般都能自動檢測到,並使一個事務釋放鎖並回退,另一個事務從而獲得鎖,得以完成事務。但在涉及外部鎖,或涉及表鎖的情況下,InnoDB 並不能完全自動檢測到死鎖, 這需要通過設置鎖等待超時參數 innodb_lock_wait_timeout 來解決,默認值是50s,參數innodb_deadlock_detect可以控制這個邏輯,默認開啓。

3.2.2 InnoDB避免死鎖的方法

  1. 可以在事務開始時通過爲預期要修改的每個行使用SELECT … FOR UPDATE語句來獲取必要的鎖,即使這些行的更改語句是在之後才執行的。
  2. 在事務中,如果要更新記錄,應該直接申請足夠級別的鎖,即排他鎖,而不應在事務中先申請共享鎖、更新時再申請排他鎖,因爲這時候當用戶再申請排他鎖時,其他事務可能又已經獲得了相同記錄的共享鎖,從而造成鎖衝突,甚至死鎖。比如事務A中有兩條操作,一條是獲取數據,一條是修改數據,那麼事務A沒有必要在獲取數據時申請共享鎖,在修改數據時申請排他鎖,只需要在事務開始時,獲取排他鎖即可。
  3. 如果事務需要修改或鎖定多個表,則應在每個事務中以相同的順序使用加鎖語句。 在應用中,如果不同的程序會併發存取多個表,應儘量約定以相同的順序來訪問表,這樣可以大大降低產生死鎖的機會。比如兩個事務A、B,都需要獲取兩個資源X、Y時,若事務A、B都以相同的順序來訪問資源X、Y,那麼就不會產生死鎖。
  4. 改變事務隔離級別。

3.3 鎖的性能優化

  關於優化的問題,其實都是具體場景具體分析,此處只給出了一些常用的經驗,具體措施,還需迴歸場景。

  1. 儘量使用較低的隔離級別;
  2. 合理設計索引,儘量讓SQL使用到索引,才能使用粒度更小的行鎖,使得加鎖更精確, 從而減少鎖衝突的機會;
  3. 合理設計事務大小,小事務間發生鎖衝突的機率也更小;
  4. 合理設計事務,要避免事務間產生死鎖;
  5. 儘量用相等條件訪問數據,從而避免間隙鎖對併發插入的影響;
  6. 不要申請超過實際需要的鎖級別;
  7. 在COMMITTED READ(讀提交)和REPEATABLE READ(可重複讀)兩種隔離級別下,因爲有MVCC機制,所以沒有特殊情況,SELECT語句不要顯示加鎖。

四、MVCC機制

  多版本併發控制,Multi-Version Concurrency Control,MVCC。MVCC 是一種併發控制的方法,實現對數據庫的併發訪問。從前文可知鎖就是控制併發操作的,但是系統開銷較大,而MVCC可以在大多數情況下代替行級鎖,以降低其系統開銷。

4.1 MVCC機制的技術背景

  在InnoDB中有四種隔離級別,其中REPEATABLE READ級別滿足事務之間互不影響,同時具有高併發性。這種效果是不能由上面介紹的悲觀鎖實現的,因爲一旦使用悲觀鎖,高併發性大大降低。本章開頭提到了一種樂觀鎖,它系統開銷小,併發性強,所以REPEATABLE READ隔離級別便使用了樂觀鎖的思想來實現,也就是本節介紹的MVCC。

4.2 MVCC機制的實現

  基於樂觀鎖的思想,MVCC也採用數據的版本號來保證數據的一致性。MVCC在每一行數據的後面隱式添加兩列數據,一列爲行數據創建時間,一列爲行數據刪除時間,存儲的是對該行數據操作的事務系統版本號,版本號是按照事務開啓的順序遞增的。爲了方便講解,此處的系統版本號用事務ID替代,如下所示:

id name 行數據創建時間 行數據刪除時間
1 Damon 1 2

  該行數據表示,該行是由事務1創建的,同時被事務2刪除了。
  多事務的情況下,如何保證一個事務能訪問到有效數據呢?需要當前事務的系統版本號大於等於創建時間,同時小於刪除時間或刪除時間爲空,那麼就表示事務可以訪問到該行數據。

4.3 MVCC運行實例

  本節將以實際的SQL操作介紹MVCC的運行機制,爲了講解清晰,系統版本號用事務ID替代。

4.3.1 MVCC運行Insert操作

  當事務1執行如下操作:

start transaction;
insert into student_table values(1,'Damon');
insert into student_table values(2,'Tom');
insert into student_table values(3,'Xuebao');
commit;

  事務1完成後,表中的數據如下所示:

id name 行數據創建時間 行數據刪除時間
1 Damon 1 undefined
2 Tom 1 undefined
3 Xuebao 1 undefined

  因爲事務1只進行了插入操作,所以表中各行數據只有創建時間被標記爲1。

4.3.2 MVCC運行Delete操作

  事務2開始執行如下操作:

start transaction;
Delete from student_table where id = 3;
commit;

  事務2完成後,表中的數據如下所示:

id name 行數據創建時間 行數據刪除時間
1 Damon 1 undefined
2 Tom 1 undefined
3 Xuebao 1 2

  因爲事務2只進行了刪除操作,所以表中id爲3的行數據刪除時間被標記爲2。

4.3.3 MVCC運行Select操作

  事務3開始執行如下操作:

start transaction;
Select * from student_table where id = 1;
Select * from student_table where id = 3;
commit;

  事務3的第一句Select:版本號爲3大於id爲1的行數據的創建時間1且刪除時間未定義,所以該語句可以訪問到id爲1的數據。
  事務3的第二句Select:版本號爲3大於id爲3的行數據的創建時間1,但大於刪除時間2,所以該語句不能訪問到id爲3的數據。
  所以事務3執行完後,應返回如下表數據:

id name 行數據創建時間 行數據刪除時間
1 Damon 1 undefined
2 Tom 1 undefined

4.3.4 MVCC運行Update操作

  InnoDB引擎在執行Update操作時,其實是新添加了一行數據,同時更新老數據行的刪除時間。
  事務4開始執行如下操作:

start transaction;
Update student_table Set name=Tom2 where id = 2;
commit;

  事務4執行後,首先將id爲2的行數據刪除時間設置爲事務4的系統版本號4,同時新插入一行原數據,創建時間爲4,刪除時間未定義,如下表所示:

id name 行數據創建時間 行數據刪除時間
1 Damon 1 undefined
2 Tom 1 undefined
3 Xuebao 1 2
2 Tom2 4 undefined

五、總結

  本文介紹了MySQL中鎖相關的知識點,以樂觀鎖、悲觀鎖爲主線,依次介紹了MySQL中的5種悲觀鎖,意向共享鎖、意向排他鎖、共享鎖、排他鎖、間隙鎖;然後分析了使用悲觀鎖時常見的三個問題,並給出了鎖的性能優化策略;最後詳細分析了MySQL的樂觀鎖,即MVCC機制,並給出增刪改查四種情況下MVCC的分析過程。

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