MySQL锁机制,行锁jingran加在索引上
鎖概述
鎖是計算機協調多個進程或線程并發訪問某一資源的機制,應該都不陌生。?但在這之前我們先來看看并發控制,理清MVCC多版本并發控制和鎖的關系,這也是之前我很迷惑的一個點
并發控制技術
在數據庫中,數據可以允許多個用戶同時訪問,因此在并發場景下需要確保數據的一致性,可以簡單梳理一下,并發場景有三種:
從寬泛意義上講,目前有三種并發控制技術:
- 悲觀并發控制(PCC):心態悲觀,假定多用戶并發的事物在處理時都會引起并發沖突,每次操作數據的時候都會上鎖
先取鎖再訪問的策略,為數據的安全提供了保證,但是加鎖會產生額外的開銷,增加死鎖的機會,只讀型事物不會產生沖突也不需要加鎖 - 樂觀并發(OCC):心態樂觀,假定多用戶并發的事物在處理時不會彼此互相影響,只在提交時檢查有沒有其它事物修改了該數據
可以獲得更大的吞吐量,但是發生沖突事物就會回滾重新執行 - 多版本并發(MVCC):每個寫操作都會創建一個新版本的數據,讀操作根據可見性規則返回其中一個數據快照
讀 - 寫沖突不加鎖,非阻塞讀的同時避免了臟讀和不可重復讀,但需要管理和挑選數據版本
對并發控制有了一定的了解,但需要注意:
- MVCC不是與悲觀和樂觀并發控制并不是對立的,很直觀的一點MVCC可以在不加鎖的情況下解決讀-寫沖突,并不能解決寫-寫沖突,寫操作還是需要上鎖
- MVCC可以與悲觀并發或樂觀并發結合使用來提高并發的性能
MySQL中實現多版本兩階段鎖協議,也就是MVCC+2PL(2PL是悲觀并發實現的一種算法,鎖只有在commit或rollback的時候釋放)
為什么需要鎖
再總結一下:
- 事物在并發場景下會發生讀-讀、讀-寫、寫-寫三種沖突,而沖突會導致臟讀、不可重復讀、幻讀以及更新丟失等一些問題
- 為了保證數據的完整性和一致性,需要使用鎖來支持對共享資源的并發訪問,結合多版本并發控制在很多情況下避免了加鎖操作
鎖分類
MySQL中鎖大致可以按照數據庫的層級分為DB級別鎖、表級別鎖以及行級別鎖,而不同的數據庫引擎支持的鎖類型也不同:
-
MyISAM 只支持到表級鎖
-
InnoDB 可以支持到行級鎖
全局鎖
在DB級別對整個數據庫實例加鎖,加鎖之后:
- 數據庫處于只讀狀態
- 阻塞對數據的增刪改以及DDL
加鎖方式:lock Flush tables with read lock
釋放鎖:unlock tables(發生異常時會自動釋放)
全局鎖主要用于做全庫的邏輯備份,和設置數據庫只讀(set global readonly=true)相比,全局鎖在發生異常時會自動釋放
MyISAM、InnoDB都支持全局鎖,但InnoDB一般不使用
基于InnoDB對事物的支持以及MVCC多版本并發的實現,InnoDB可以選擇mysqldump工具加 –single-transaction參數,在不阻塞寫操作的同時做全庫的邏輯備份
表級別鎖
表級別對操作的整張表加鎖,鎖定顆粒度大,資源消耗少,不會出現死鎖,但并發度低,表級鎖有兩種模式:
- 表共享鎖:對同一表的操作不阻塞讀,阻塞寫
- 表獨占鎖:對同一表的操作讀寫阻塞
MyISAM引擎默認支持表級別鎖
表級別的鎖有兩種:表鎖和元數據鎖(MDL)
表鎖
顯示加鎖方式:lock tables {tb_name} read/write
釋放鎖:unlock table {tb_name} (連接中斷也會自動釋放)
MyISAM引擎下隱式加鎖:
- 執行SELECT查詢自動加共享鎖(讀鎖)
- 執行INSERT、UPDATA、DELETE操作自動加獨占鎖(寫鎖)
MyISAM讀寫鎖優先級:
默認情況下寫鎖比讀鎖具有更高的優先級,即使讀請求先到等待隊列,寫鎖也會插入到讀鎖之前,優先執行寫操作,但MyISAM也支持依據生產環境通過修改參數的設置改變讀寫的優先級
元數據鎖(MDL)
隱式鎖,主要針對對表結構改變的操作(DDL),沒有顯示加鎖方式,訪問表時自動加鎖:
- 執行DML(SELECT, INSERT…) 操作加共享鎖(讀鎖)
- 執行DDL(ALTER, DROP…) 操作加獨占鎖(寫鎖)
到這里你是不是會有疑問:假設我要向表里增加一個字段隱式加MDL寫鎖,那么線上所有對這個表的增刪改查(DML)操作都會阻塞
MySQL在5.6之后引入online DDL,也就是進行DDL操作時MDL寫鎖會降級成讀鎖,線上DML操作不會被阻塞,DDL操作完成之后升級回MDL寫鎖然后釋放
查看表級鎖爭用情況:SHOW STATUS LIKE ‘table%’
總之表級鎖因為鎖的粒度大,若一個事物執行時間過長,很可能會導致后面對這個表的請求全部阻塞
行級別鎖
InnoDB支持行級別鎖,鎖粒度小并發度高,但是加鎖開銷大也很可能會出現死鎖,鎖模式:
- 共享鎖(讀鎖) S:對同一行的操作讀不阻塞,阻塞寫
- 排它鎖(寫鎖) X:對同一行的操作讀寫都會阻塞
- 意向共享鎖 IS:一個事物想要加S鎖時必須先獲得該表的IS
- 意向排它鎖 IX:一個事物想要加X鎖時必須先獲得該表的IX
為什么需要意向鎖:
意向鎖是表級別的鎖,用來標識該表上有數據被鎖住或即將被鎖,對于表級別的請求(LOCK TABLE…),就可以直接判斷是否有鎖沖突,不需要逐行檢查鎖的狀態
InnoDB的默認隔離級別RR(可重復讀),在RR下讀數據有兩種方式:
- 快照讀:在MVCC下,事物開啟執行第一個SELECT語句后會獲取一個數據快照,直到事物結束讀取到的數據都是一致的
普通的 select… 查詢都是快照讀 - 當前讀:讀取的數據的最新版本,并且在讀的時候不允許其它事物修改當前記錄
select… lock in share mode(讀鎖)
select… for update(寫鎖)
加鎖方式:
- 普通 select… 查詢 (不加鎖)
- 普通 insert、update、delete… (隱式加寫鎖)
- select…lock in share mode (加讀鎖)
- select…for update (加寫鎖)
解鎖:
提交/回滾事物(commit/rollback)
kill 阻塞進程
注:以下行級鎖分析都默認RR(可重復讀)的事物隔離級別
鎖加在索引上
InnoDB的行鎖是通過給索引上的索引項加鎖來實現的
即使在建表的時候沒有指定主鍵,InnoDB會默認創建一個DB_ROW_ID的自增字段為表的主鍵,并且其主鍵索引(聚簇索引)為GEN_CLUST_INDEX
主鍵索引也被稱為聚簇索引
可以看下面例子,涉及到回表對聚簇索引的索引項也會加鎖:
行級鎖算法:
- Record Lock: 對對應的索引記錄項加鎖
- Gap Lock:對索引項之間的間隙加鎖,加鎖之后間隙范圍內不允許插入數據,防止發生幻讀
- Next-key Lock:可以理解為Record Lock+Gap Lock(InnoDB行鎖默認加的是 Next-key Lock)
舉個例子更好理解:
現在你可能已經知道了:
如果在加Record Lock的基礎之上再加上Gap Lock問題就解決了
通過上面這個例子,我們可以看到:
-
record lock 可以鎖一個存在的索引項
-
gap lock 鎖索引項之間的間隙,可以防止幻讀(左開右開區間)
-
next-key lock 上面兩個鎖相加,innodb默認加鎖單位(左開右閉區間)
加鎖規則
行級鎖默認加 next-key lock,查詢過程中訪問到的索引項都會加鎖,而根據不同的索引也有不同的加鎖規則:
- 唯一索引等值查詢:當索引項存在時,next-key lock 退化為 record lock;當索引項不存在時,默認 next-key lock,訪問到不滿足條件的第一個值后next-key lock退化成gap lock
- 唯一索引范圍查詢:默認 next-key lock,(特殊’<=’ 范圍查詢直到訪問不滿足條件的第一個值為止)
- 非唯一索引等值查詢:默認next-key lock ,索引項存在/不存在都是訪問到不滿足條件的第一個值后next-key lock退化成gap lock
- 非唯一索引范圍查詢:默認 next-key lock,向右訪問到不滿足條件的第一個值為止
注:以上加鎖規則參考《mysql 45講》和實踐驗證自己總結所得,非官方規則
可能有點難理解,針對這幾種情況分別舉例說明一下,假設我有以下數據:
| 1 | 張三 | 21 |
| 4 | 王一 | 26 |
| 6 | 小軍 | 18 |
| 9 | 小紅 | 23 |
在上面的數據表我們可以得到5個next-key lock 區間:
唯一索引(id):(-∞,1],(1,4],(4,6],(6,9] ,(9,+supremum]
非唯一索引(age):(-∞,18],(18,21],(21,23],(23,26] ,(26,+supremum]
唯一索引等值查詢:
唯一索引范圍查詢:
非唯一索引等值查詢:
非唯一索引范圍查詢:
細心一點你會發現上面例子中:
- 唯一索引的查詢用的是 select … for update
- 非唯一索引的查詢用的是 select … lock in share model
for update 加的是寫鎖,寫鎖默認認為會對數據做更改,不管查詢有沒有涉及到回表都會對聚簇索引(主鍵索引)加鎖
lock in share model 加的是讀鎖,如果沒有涉及到回表(像覆蓋索引),不會對聚簇索引(主鍵索引)加鎖
如果上面例子中非唯一索引的查詢用的是 select … for update,還需要分析聚簇索引(主鍵索引)的加鎖情況
死鎖
死鎖指的是兩個或兩個以上的事物在執行過程中爭搶鎖資源而造成相互等待的情況
表鎖不會出現死鎖,主要還是針對InooDB的行鎖,可以看下面的例子:
監控分析鎖問題
# 查詢InnoDB鎖的整體情況 # 可以重點查看Innodb_row_lock_waits和Innodb_row_lock_time_avg這兩個值 # 如果數值較大,說明鎖之間的競爭大 show status like 'innodb_row_lock%';#可以通過INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS這三個表 #分析可能存在的鎖的問題 select * from information_schema.INNODB_TRX; # 查看所有事物 select * from information_schema.INNODB_LOCKS; # 查看鎖 select * from information_schema.INNODB_LOCK_WAITS; # 查看鎖等待解決死鎖:
-
超時等待,事物超時自動回滾(innodb_lock_wait_timeout 默認50s)
-
主動死鎖檢測,事物請求鎖的時候采用 wait-for graph 等待圖的方式進行死鎖檢測(innodb_deadlock_detect 默認on)
-
發現死鎖也可以人為 kill 進程
總結
-
MySQL鎖分為全局鎖、表級鎖以及行級鎖,不同的存儲引擎支持鎖的粒度有所不同,MyISAM 只支持到表級鎖,InnoDB 則可以支持到行級鎖,鎖的粒度決定了業務的并發度,因此更推薦使用InnoDB
-
InnoDB默認最小加鎖粒度為行級鎖,并且鎖是加在索引上,如果SQL語句未命中索引,則走聚簇索引的全表掃描,表上每條記錄都會上鎖,導致并發能力下降,增大死鎖的概率,因此需要為表合理的添加索引,線上查詢盡量命中索引
-
行級鎖默認加 next-key lock,而根據不同的索引也有不同的加鎖規則,我們可以根據加鎖規分析加鎖區間
-
鎖粒度的減小提高了并發度的同時也增加了死鎖的風險,查詢應盡量考慮減少鎖的范圍
總結
以上是生活随笔為你收集整理的MySQL锁机制,行锁jingran加在索引上的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: matlab-游标及查询
- 下一篇: 在SpringBoot中使用slf4j与