1. 程式人生 > >MySQL InnoDB鎖機制分享

MySQL InnoDB鎖機制分享

寫在前面:在設計新零售供應鏈wms(倉庫管理系統)庫存模組時,為了防止併發情況對庫存的影響,查閱了一些資料,對InnoDB鎖機制有了更全面的瞭解,在此做出分享,如有疏漏望不吝指正,願共同進步!(此篇為1.0版本,後續隨理解深入,會逐步迭代完善~)

一、為什麼要加鎖

鎖機制用於管理對共享資源的併發訪問。

當多個使用者併發地存取資料時,在資料庫中就可能會產生多個事務同時操作同一行資料的情況,若對併發操作不加控制就可能會讀取和儲存不正確的資料,破壞資料的一致性。

一種典型的併發問題——丟失更新(其他鎖問題及解決方法會在後面說到):

注:RR預設隔離級別下,為更清晰體現時間先後,暫時忽略鎖等待,不影響最終效果~

時間點 事務A 事務B
1 開啟事務A
2 開啟事務B
3 查詢當前商品S庫存為100
4 查詢當前商品S庫存為100
5 業務邏輯處理,確定要將商品S庫存增加10,故更新庫存為110(update stock set amount=110 where sku_id=S;)
6 業務邏輯處理,確定要將商品S庫存增加20,故更新庫存為120(update stock set amount=120 where sku_id=S;)
7 提交事務A
8 提交事務B

異常結果:商品S庫存更新為120,但實際上針對商品S進行了兩次入庫操作,最終商品S庫存應為100+10+20=130,但實際結果為120,首先提交的事務A的更新『丟失了』!!!所以就需要鎖機制來保證這種情況不會發生。

二、InnoDB鎖型別概述
這裡寫圖片描述

簡介(後面會分別詳細說到):

1、樂觀鎖與悲觀鎖是兩種併發控制的思想,可用於解決丟失更新問題:

樂觀鎖會“樂觀地”假定大概率不會發生併發更新衝突,訪問、處理資料過程中不加鎖,只在更新資料時再根據版本號或時間戳判斷是否有衝突,有則處理,無則提交事務;

悲觀鎖會“悲觀地”假定大概率會發生併發更新衝突,訪問、處理資料前就加排他鎖,在整個資料處理過程中鎖定資料,事務提交或回滾後才釋放鎖;

2、InnoDB支援多種鎖粒度,預設使用行鎖,鎖粒度最小,鎖衝突發生的概率最低,支援的併發度也最高,但系統消耗成本也相對較高;
3、共享鎖與排他鎖是InnoDB實現的兩種標準的行鎖;
4、InnoDB有三種鎖演算法——記錄鎖、gap間隙鎖、還有結合了記錄鎖與間隙鎖的next-key鎖,InnoDB對於行的查詢加鎖是使用的是next-key locking這種演算法,一定程度上解決了幻讀問題;

三、行鎖詳解

InnoDB預設使用行鎖,實現了兩種標準的行鎖——共享鎖與排他鎖;
這裡寫圖片描述
注意:
1、除了顯式加鎖的情況,其他情況下的加鎖與解鎖都無需人工干預。
2、InnoDB所有的行鎖演算法都是基於索引實現的,鎖定的也都是索引或索引區間(這一點會在第六章節『鎖演算法』中詳細說到);

共享鎖與排它鎖相容性示例(使用預設的RR隔離級別,圖中數字從小到大標識操作執行先後順序):
這裡寫圖片描述

四、當前讀與快照讀

1、當前讀:即加鎖讀,讀取記錄的最新版本,會加鎖保證其他併發事務不能修改當前記錄,直至獲取鎖的事務釋放鎖;

使用當前讀的操作主要包括:顯式加鎖的讀操作與插入/更新/刪除等寫操作,如下所示:

select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;

注:當Update SQL被髮給MySQL後,MySQL Server會根據where條件,讀取第一條滿足條件的記錄,然後InnoDB引擎會將第一條記錄返回,並加鎖,待MySQL Server收到這條加鎖的記錄之後,會再發起一個Update請求,更新這條記錄。一條記錄操作完成,再讀取下一條記錄,直至沒有滿足條件的記錄為止。因此,Update操作內部,就包含了當前讀。同理,Delete操作也一樣。Insert操作會稍微有些不同,簡單來說,就是Insert操作可能會觸發Unique Key的衝突檢查,也會進行一個當前讀。

2、快照讀:即不加鎖讀,讀取記錄的快照版本而非最新版本,通過MVCC實現;

InnoDB預設的RR事務隔離級別下,不顯式加『lock in share mode』與『for update』的『select』操作都屬於快照讀,保證事務執行過程中只有第一次讀之前提交的修改和自己的修改可見,其他的均不可見;

五、MVCC

MVCC『多版本併發控制』,與之對應的是『基於鎖的併發控制』;

MVCC的最大好處:讀不加任何鎖,讀寫不衝突,對於讀操作多於寫操作的應用,極大的增加了系統的併發效能;

InnoDB預設的RR事務隔離級別下,不顯式加『lock in share mode』與『for update』的『select』操作都屬於快照讀,使用MVCC,保證事務執行過程中只有第一次讀之前提交的修改和自己的修改可見,其他的均不可見;

關於InnoDB MVCC的實現原理,在《高效能Mysql》一書中有一些說明,網路上也大多沿用這一套理論,但這套理論與InnoDB的實際實現還是有一定差距的,但不妨我們通過它初步理解MVCC的實現機制,所以我在此貼上此書中的說明;
這裡寫圖片描述

六、鎖演算法

InnoDB主要實現了三種行鎖演算法:
這裡寫圖片描述
InnoDB所有的行鎖演算法都是基於索引實現的,鎖定的也都是索引或索引區間;

不同的事務隔離級別、不同的索引型別、是否為等值查詢,使用的行鎖演算法也會有所不同;下面僅以InnoDB預設的RR隔離級別、等值查詢為例,介紹幾種行鎖演算法:
這裡寫圖片描述

1、等值查詢使用聚簇索引
這裡寫圖片描述

注: InnoDB表是索引組織表,根據主鍵索引構造一棵B+樹,葉子節點存放的是整張表的行記錄資料,且按主鍵順序存放;我這裡做了一個表格模擬主鍵索引的葉子節點,使用主鍵索引查詢,就會鎖住相關主鍵索引,鎖住了索引也就鎖住了行記錄,其他併發事務就無法修改此行資料,直至提交事務釋放鎖,保證了併發情況下資料的一致性;

2、等值查詢使用唯一索引
這裡寫圖片描述

注:輔助索引的葉子節點除了存放輔助索引值,也存放了對應主鍵索引值;鎖定時會鎖定輔助索引與主鍵索引;

3、等值查詢使用輔助索引
這裡寫圖片描述

注:Gap鎖,鎖定的是索引記錄之間的間隙,是防止幻讀的關鍵;如果沒有上圖中綠色標識的Gap Lock,其他併發事務在間隙中插入了一條記錄如:『insert into stock (id,sku_id) values(2,103);』並提交,那麼在此事務中重複執行上圖中SQL,就會查詢出併發事務新插入的記錄,即出現幻讀;(幻讀是指在同一事務下,連續執行兩次同樣的SQL語句可能導致不同的結果,第二次的SQL語句可能返回之前不存在的行記錄)加上Gap Lock後,併發事務插入新資料前會先檢測間隙中是否已被加鎖,防止幻讀的出現;

七、鎖問題
MySQL鎖會帶來如下幾種問題,如果能解決他們,就可以保證併發情況下不會出現問題;

鎖問題 鎖問題描述 會出現鎖問題的隔離級別 解決辦法
髒讀 一個事務中會讀到其他併發事務未提交的資料,違反了事務的隔離性; Read Uncommitted 提高事務隔離級別至Read Committed及以上;
不可重複讀 一個事務會讀到其他併發事務已提交的資料,違反了資料庫的一致性要求;可能出現的問題為幻讀,幻讀是指在同一事務下,連續執行兩次同樣的SQL語句可能導致不同的結果,第二次的SQL語句可能返回之前不存在的行記錄; Read Uncommitted、Read Committed 預設的RR隔離級別下 ,解決辦法分為兩種情況:1、當前讀:Next-Key Lock機制對相關索引記錄及索引間隙加鎖,防止併發事務修改資料或插入新資料到間隙;(詳情參見第六章節『鎖演算法』)2、版本讀:MVCC,保證事務執行過程中只有第一次讀之前提交的修改和自己的修改可見,其他的均不可見;提高事務隔離級別至Serializable;
丟失更新 見章節一中描述; Read Uncommitted、Read Committed、Repeatable Read 預設的RR隔離級別下 ,解決辦法分為兩種情況:1、樂觀鎖:資料表增加version欄位,讀取資料時記錄原始version,更新資料時,比對version是否為原始version,如不等,則證明有併發事務已更新過此行資料,則可回滾事務後重試直至無併發競爭;2、悲觀鎖:讀加排他鎖,保證整個事務執行過程中,其他併發事務無法讀取相關記錄,直至當前事務提交或回滾釋放鎖;詳情可參看部落格:https://segmentfault.com/a/1190000012939310

注:其實InnoDB預設的RR事務隔離級別已經為我們做了大多數的事,業務中更多需要關心『丟失更新』這種問題,通常使用樂觀鎖方式解決;我們在讀操作時一般不會使用加鎖讀,但MVCC並不能完全解讀幻讀問題,其他併發事務是可以插入符合當前事務查詢條件的資料,只是當前事務因為讀快照資料無法檢視到,這種情況下應該使用唯一索引等方式保證不會重複插入重複的業務資料,在此不再贅述~

參考及推薦