通過本文我們將瞭解以下幾個問題?
一、思考
問題一:多線程下,MYSQL是如何同步的?
比如多個線程同時要更新某一行數據,這時候MYSQL的處理機制是什麼?
問題二:MYSQL中有哪些鎖?其作用都是什麼?
相比大家聽說過行鎖、表鎖、樂觀鎖、悲觀鎖、間隙鎖、死鎖、共享鎖、排它鎖這些概念,本文將探索這些鎖是幹嘛的。
二、分析
接下來我們將一一揭開MYSQL中各種鎖的面紗。
2.1行鎖和表鎖
針對鎖粒度的不同,分別有行鎖和表鎖之分,表鎖針對的是整個表,而行鎖針對的是數據行。MYSIAM採用表鎖機制,而InnoDB默認採用行鎖機制,但是InnoDB也支持表鎖。
行級鎖和表級鎖比較:
表級鎖
優點
- 加鎖速度更快,開銷更小。
- 不會產生死鎖情況。
缺點
- 因爲其針對整個表加鎖,所以鎖粒度更大,併發情況下競爭鎖時衝突概率更高,即併發度更低。
行級鎖
優點
- 鎖粒度更小,併發情況下競爭鎖時衝突概率更低,即併發度更高。
缺點
- 加鎖速度慢,開銷大。
- 可能產生死鎖。
這裏爲什麼表鎖不會產生死鎖呢,假如一個線程獲取鎖的順序是A表,B表,另一線程是B表,A表,不是仍然可能死鎖嗎?
這是因爲MyISAM存儲引擎通過總是一次性同時獲取所有需要的鎖以及總是按相同的順序獲取表鎖來避免死鎖。
MySIAM中表級鎖的實現模式:
表共享讀鎖:當查詢某個表,MySIAM會給此表加讀鎖,允許其他線程對該表的讀操作,阻塞對該表的寫操作。
表獨佔寫鎖:當更新某個表數據時,MySIAM會給該表加寫鎖,阻塞其他線程對該表的讀和寫。
MyISAM存儲引擎中默認寫鎖的優先級會高於讀鎖。但是也可以通過以下參數降低更新操作的優先級,避免查詢時間太長,導致查詢線程餓死情況。具體操作請參考博文:https://www.cnblogs.com/zhoujinyi/p/3337347.html。
如何加表鎖:
MySIAM存儲引擎在讀取數據時,會自動爲表加共享讀鎖,更新數據時爲表加獨佔寫鎖,因此一般情況下不需要手動加表鎖。那如何採用手動方式加表鎖呢?可以通過如下方式:
加共享讀鎖:lock tables t read/read local;read和read local的區別在於對於read local這種共享讀鎖,不會阻塞其它線程的插入操作,但是更新操作依然阻塞。
加獨佔寫鎖:lock tables t write;
2.2共享鎖和排它鎖
上文中介紹了行鎖和表鎖的區別,以及MySIAM中表鎖的鎖模式,該節將介紹InnoDB行鎖的實現模式:共享鎖和排它鎖。
共享鎖和排它鎖的比較:
- 共享鎖(S鎖):當某個事務獲取共享鎖,允許其他事務讀一行和加共享鎖,但是禁止其他事務獲取相同數據的排它鎖和修改數據,當前事務也不可修改數據。
- 排它鎖(X鎖):當某個事務獲取排它鎖,其它事務無法獲取相同數據的共享鎖和排它鎖,其他事務可以讀取數據,但是不能修改數據,當前事務可修改數據。
除了共享鎖和排它鎖,InnoDB爲了支持行鎖和表鎖共存特性(多粒度鎖),還提供了意向共享鎖和意向排它鎖,意向鎖(Intention Locks)是一種不與行級鎖衝突的表級鎖。我們先了解一下意向鎖。
意向共享鎖和意向排它鎖
- 意向共享鎖(IS):事務有意向給數據行加共享鎖(S),在加共享鎖之前必須先獲得該表的意向共享鎖。
- 意向排它鎖(IX):事務有意向給數據行加排它鎖(X),在加排它鎖之前必須先獲得該表的意向排它鎖。
互斥性
備註:該圖來源於博文《MYSQL鎖總結:https://zhuanlan.zhihu.com/p/29150809》
兼容:代表獲取鎖成功。
衝突:獲取鎖失敗,阻塞。
注意:
- 意向鎖和意向鎖之前是不會產生互斥的
- 意向鎖和非意向鎖之間的互斥針對的是表鎖,不同的事務對於不同的行加鎖是不會互斥的。
意向鎖存在的意義
上問分析過假如事務A要修改表t的某一行,必然獲取該行數據的X鎖和該表的IX鎖。但是因爲意向鎖之間不互斥,其他事務要修改該t表的另一行數據,當然不會阻塞,因爲其獲取IX鎖和X鎖都將成功。那麼意向鎖存在的意義是什麼?
假如有這種情況:事務A要修改表t的某一行,必然獲取該行數據的X鎖和該表的IX鎖。而事務B執行lock tables t write,即事務表要鎖定該表。那麼事務B如何得知該表有沒有已經被鎖定呢?最快的方式當然是判斷該表是否已經加了IX鎖,而不是遍歷該表是否有一行數據加了X鎖。這也說明了IX鎖和X鎖互斥,而且互斥針對的是表鎖。
如何加共享鎖和排它鎖
意向鎖是InnoDB引擎內部的實現機制,不需要開發者去做任何事情。所以這裏只討論共享鎖和排它鎖的加鎖方式:
對於更新操作(UPDATE\DELETE\INSERT)操作,InnoDB會自動爲數據行加排它鎖(X),無須開發者干預。
對於查詢操作(SELECT),InnoDB並未對其加任何鎖,開發者可以手動加鎖(顯示鎖定),方式如下:
共享鎖(S):SELECT * FROM t WHERE id=1 LOCK IN SHARE MODE;此時其他事務仍然可以查詢該數據,或者並在該數據上加共享鎖;但是如果當前事務內後續有對於該行的更新操作則有可能造成死鎖。
排它鎖(X):SELECT * FROM t WHERE id=1 FOR UPDATE;此時其他事務是可以查詢到該數據的,但是如果其他事務也要獲取共享鎖或者排它鎖,將被阻塞。
此外只有根據索引檢索數據時,InnoDB引擎纔會使用行鎖,否則將使用表鎖。
2.3樂觀鎖和悲觀鎖
樂觀鎖和悲觀鎖比較
樂觀鎖:類似於JAVA中的CAS失敗重試機制,樂觀的認爲在同一時刻併發的情況還是少數,不顯示加鎖,可以通過版本控制的方式實現(稍後介紹)。所以這只是一種思想,並未正真加鎖。
悲觀鎖:悲觀鎖認爲接下來的操作一定會產生併發問題,所以提前加上排它鎖。
樂觀鎖和悲觀鎖的加鎖方式
樂觀鎖(版本控制):
假如要採用樂觀鎖機制更新表t的某個字段x,可以爲該表添加一個version字段。採用如下方式更新:
UPDATE t SET x='a' WHERE id=1 AND version=#{version},version爲當前事務查詢出來的值。如果更新失敗,則說明已經有其他事務修改了該數據行。
悲觀鎖(排它鎖):
在更新數據之前直接加鎖,可以利用上節所講行鎖排它鎖:
SELECT x FROM t WHERE id=1 FOR UPDATE;獲取排它鎖,如果獲取成功可以安全的更新,如果獲取失敗,說明其他事務已經獲取排它鎖,當前事務將阻塞。
2.4間隙鎖
概念
何爲“間隙”?
當我們根據索引進行範圍查詢時,比如有表t,id爲主鍵索引,age爲非唯一普通索引(唯一索引會降級爲行鎖)。存在如下記錄:
(1,10),(2,20),(50);
有如下查詢語句:SELECT name FROM t WHERE age>=20 AND id<=50;
那麼在20<id<50之間就被稱之爲“間隙”。當你執行上述語句時,InnoDB引擎會自動爲你加上間隙鎖,防止併發情況下其他事務插入間隙範圍內的數據。
InnoDB支持的行鎖定方式有:
行鎖(Record Lock):鎖定某一行記錄。
間隙鎖(GAP Lock):鎖定間隙範圍內記錄。
行鎖和間隙鎖組合(Next -key Lock):自動升降級爲間隙鎖和行鎖。
加鎖時機
除了上文所說的範圍檢索以外,在以下情況下會加間隙鎖:
- 範圍查詢,對於範圍內不存在的記錄加間隙鎖。
- 指定條件查詢,對於查詢結果不存在的記錄也會加間隙鎖。
間隙鎖的意義
- InnoDB存儲引擎默認的事務隔離級別爲可重複讀(RR),而其默認會使用Next-key Lock來防止幻讀的產生。
- 保證MySQL主從複製的正確性,具體請參考:https://www.cnblogs.com/rjzheng/p/10510174.html
2.5死鎖
死鎖的一般概念
有線程A和B,共享資源S1和S2且資源同時只能被某一個線程佔用,A持有資源S1等待獲取資源S2,B持有資源S2等待獲取S1,這時A和B將互相等待,若無外界干擾,這種情況將無限期持續,造成死鎖。
死鎖產生的必要條件:
- 互斥條件:即資源在某段時間內只能由一個線程佔用。比如上述S1同時只能被A所佔用。
- 不可搶佔條件:即線程已經擁有的資源在未被自己釋放之前,不可由其它線程搶佔。比如上述A擁有S1在A未主動釋放之前,B不能搶奪。
- 請求和保持條件:即某個線程已經至少擁有了一個資源,又再次請求其它資源。比如上述A已經擁有S1再次請求S2。
- 循環等待條件。即發生死鎖時必然存在多個線程循環等待的情況。比如上述A等待被B佔用的資源S2,B等待被A佔用的資源S1,造成循環。
InnoDB中死鎖發生條件
假如有表t1,字段id,age(唯一索引),存在數據:1,18),(2,20)表t2,字段id,tel(唯一索引),存在數據:(2,188),(2,187)
現有事務A和事務B。情況如下:
事務A:
- 執行SELECT * FROM t1 WHERE age=18 FOR UPDATE,獲取排它鎖,滿足互斥條件和不可搶奪條件;
- 執行SELECT * FROM t2 WHERE tel=188 FOR UPDATE,滿足請求和保持條件。
事務A:
- 執行SELECT * FROM t1 WHERE tel=188 FOR UPDATE,獲取排它鎖,滿足互斥條件和不可搶奪條件;
- 執行SELECT * FROM t2 WHERE age=18 FOR UPDATE,滿足請求和保持條件。
假設兩個事務併發執行,則很大可能造成循環等待條件,那麼就會觸發死鎖產生。
這只是根據死鎖產生的必要條件列舉了死鎖發生的可能情況之一。在MYSQL中其他可能產生死鎖的情況如下:
- 不同表相同的記錄行鎖衝突。
- 相同表不同記錄行鎖衝突。
- 間隙鎖造成鎖衝突。
如何排查和避免死鎖
我們知道死鎖處理方式有兩種:
- 預防方式,防患於未然。
- 死鎖恢復,已經發生死鎖之後如何處理。
這裏轉自博文:https://www.aneasystone.com/archives/2018/04/solving-dead-locks-four.html,如果您有興趣,可仔細閱讀一下。
三、其他
行鎖升級爲表鎖
因爲MYSQL的行鎖是針對索引加的鎖,如果檢索條件不走索引或者索引失效,那麼就會從行鎖升級爲表鎖。
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
參考博文:
- MYSQL鎖總結:https://zhuanlan.zhihu.com/p/29150809
- 詳解MYSQL中意向鎖的作用:https://juejin.im/post/5b85124f5188253010326360
- MYSQL間隙鎖原理:https://www.cnblogs.com/aspirant/p/9177978.html
- 解決死鎖之路:https://www.aneasystone.com/archives/2018/04/solving-dead-locks-four.html。
- MYSQL應該選什麼事務隔離級別:https://www.cnblogs.com/rjzheng/p/10510174.html。
- 常見SQL語句的加鎖分析:https://www.aneasystone.com/archives/2017/12/solving-dead-locks-three.html。