你好,我是yes。java
前段時間寫了一篇關於 MySQL 鎖的文章,一些小夥伴們在閱讀以後產生了一些疑問,這些問題還挺有表明性的,因此在這裏作個實驗,來用事實探究一番。微信
那篇文章提到了記錄鎖(Record Locks),顧名思義鎖的是記錄,做用在索引上的記錄。優化
鎖是做用在索引上這句話可能不太好理解,而且對於在可重複讀和讀提交兩個隔離級別下,關因而否命中二級索引的鎖之間的阻塞也不太清晰。url
這句話讀着可能有點拗口,沒事,我來給你看幾個實驗,對這一切就異常清晰了。.net
實驗的 MySQL 版本爲:5.7.26。3d
實驗一:隔離級別爲讀提交,鎖定非索引列的實驗code
先建個很是簡單的表,只有主鍵索引,沒有二級索引。中間件
CREATE TABLE `yes` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(45) DEFAULT NULL, `address` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4
索引
隔離級別以下:事務
關閉自動提交事務:
已經準備好的數據:
此時,發起事務 A,執行以下語句,且事務未提交:
接着,再發起事務 B,執行以下語句:
你可能覺得事務 B 不會被阻塞,由於事務 B 鎖的是name=xx
和事務A鎖name=yes
講道理相互之間沒有衝突,可是從結果來看,事務 B 被阻塞了,調用select * from innodb_lock_waits;
看下誰等誰
能夠看到,事務6517(B)在等待事務6516(A)。
此時,調用 SELECT * FROM innodb_locks;
查看相關鎖的信息
鎖的類型就是行級鎖,此時的鎖爲 X 鎖,鎖的索引就是主鍵索引,這個結果代表的意思是事務 B(6517)想要 id 爲 1 的記錄鎖,可是這個記錄此時被事務A(6516)佔有。
是的,這裏的 1 其實不是指第一個記錄的意思,是 id 爲 1 的記錄。
可能有人疑惑, 爲啥 lock_data 爲 1 ?
(我沒看過源碼,我的推斷以下:)執行 select ... for update
,因爲 name 字段沒有索引,索引事務 A、B 只能加鎖到主鍵索引上,此時須要搜索 name 爲 yes 的記錄,可是又沒有索引,只能全表掃描,恰巧掃描第一條記錄就符合要求了,因而上鎖,而後接着日後掃描,後面不符合條件因此沒有上鎖。此時事務 B 加鎖,過程和事務 A 同樣須要從第一條記錄開始掃描上鎖,但此時第一條記錄已經被事務 A 鎖了,因此第一條記錄就衝突了,而第一條記錄的 id 就是爲 1,所以 lock_data 爲 1。
如今,我把事務 A 提交,則事務 B 裏面能立馬獲得結果。
從上面這個實驗能夠得知,若是查詢條件上鎖,可是沒有對應的二級索引能夠命中,那麼鎖就會鎖到主鍵(聚簇)索引上。
而聚簇索引的非葉子節點只有主鍵的信息,沒有 name 的信息,因此只能按順序的全表掃描,加鎖符合條件的記錄,可是在掃描過程當中遇到已經被加鎖的記錄就會被阻塞,即便這個記錄不是目標記錄。
看下面這個實驗,你就清晰了。
這個實驗其實就是把事務 A、B的語句執行的順序換了一下。
此時,新起一個事務 C,先執行以下語句,鎖的是id爲2的記錄:
而後,再起一個事務 D,執行:
此時一樣被阻塞了,可是查看下鎖信息你會發現:
lock_data 變爲 id 爲 2 的記錄了,也就是說事務 C 掃描了 id 爲 1 的記錄以後,發現不符合條件,就釋放了,(否則 lock_data 的值應該爲 1)而後繼續掃描 id 爲 2 的記錄,符合條件,因而上鎖。
而事務 D 也掃描了 id 爲 1 的記錄,符合條件,因而上鎖,而後接着向後掃描到 id 爲 2 的記錄,可是此時已經被事務C 加鎖了,因而被阻塞。
這結果也符合了我上面的推斷。
咱們再繼續實驗。
此次來試試 update 的,此時新起事務 E :
再起一個事務 F :
並無發生阻塞,這實際上是符合咱們預期的。但從中咱們能夠得知,在讀提交級別下,即便沒有索引,update 的全表掃描並非和select ... for update
那樣全表按順先加鎖再判斷條件,而是先找到符合的記錄,而後再上鎖。
咱們再繼續實驗。
此時,把上面的事務都提交以後,再新起一個事務 G 執行如下語句,且不提交事務:
接着,再起一個事務 H 執行如下語句:
能夠看到,事務 H 沒有被阻塞,絲滑。
說明在讀提交級別下,鎖的只是已經存在的記錄,對於插入仍是防不住的,即便插入的 name 是 yes,也同樣不會被阻塞。
實驗二:隔離級別爲可重複讀,鎖定非索引列的實驗
隔離級別爲可重複讀:
仍是以前的數據:
此時,發起事務 A,執行以下語句,且事務未提交:
接着,再發起事務 B,執行以下語句:
意料之中的結果,即事務 B 被阻塞,鎖信息以下,仍是 id 爲 1 的記錄出了鎖衝突。
此時提交事務A、B,而後再新起一個事務 C:
而後再新起一個事務 D:
沒錯,事務 C、D 就是和 A、B 來個反順序執行,重點來了,此時的鎖信息以下:
能夠看到,衝突的仍是 id 爲 1 的這條記錄,那說明事務 C 在全表掃描,從第一條開始遍歷,即便訪問到了不符合條件的記錄,加鎖以後在事務提交以前就不會釋放!
這裏就和讀已提交有差異了。
咱們再繼續實驗,此時提交事務A、B、C、D以後,再新起一個事務 E:
接着,再起事務 F 執行以下語句:
能夠看到,事務 F 被阻塞了,此時再看下鎖的一些信息:
起衝突的 lock_data 是最大記錄(supremum),這個記錄以前的文章提過的,MySQL頁默認有最大和最小兩條記錄,不存儲數據,做用相似於鏈表的 dummy 節點。
從這個結果來看,這個最大記錄也被事務 F 鎖了,這個表的 ID 是自增的,因此此時的插入記錄,恰好要插入到最後面,這樣就發生了衝突。
這其實有點出乎個人意料,我覺得事務 F 插入應該是被事務 E 加的間隙鎖給擋了纔對。
這時候,我又作了個實驗,我先造了一條 id 爲 6 的記錄,此時表內的數據以下:
一樣再起一個事務執行,且未提交:
接着,我再起一個事務執行插入,可是指明瞭插入的 id 是 4 ,這樣這條記錄會將插入到記錄 id 爲 6 的前面。
此時被阻塞了,查看鎖信息:
看到截圖的 X,GAP 沒,結果顯示插入的事務須要記錄鎖+間隙鎖,可是被前一個事務佔用的 id 爲 6 的記錄鎖給阻塞了。
這涉及到個人盲區了,上面的插入還只要記錄鎖,這時候的插入就又要申請間隙鎖了?可是也不是由於間隙被阻塞啊?我以後再找個時間研究下,若是有大佬知道,請評論區指導我下。
咱們再繼續實驗,清理下數據,還原到初始狀態:
啓動一個事務 G 執行:
接着再啓動一個事務 H 執行:
此時發生了阻塞,看下鎖的信息:
能夠看到,可重複讀級別下 update 的加鎖與讀提交不太同樣,加鎖的 lock_data 是 1,說明事務 G 掃描的 id 爲 1 的記錄以後沒有釋放鎖。
若是把事務G、H 的啓動順序反過來,也就是先執行 H 的語句再執行 G 的語句,結果也是同樣的,一樣加鎖的 lock_data 是 1,這說明可重複讀的 update 不是先判斷條件是否符合再上鎖,而是先上鎖再判斷條件是否符合。
update 都會被阻塞,最終結論就是:
在可重複讀級別下,加鎖非索引列致使的全表記錄上鎖會使得全部插入和修改都會被阻塞。
小結一下:
此時把讀者問題列上:
留言的回答語境是在可重複讀級別下,如今我再來總結回答下:
在讀提交級別下:
若是鎖定的列爲非索引列,加鎖都是加到主鍵索引上的,select ..for update
的加鎖的順序是從前日後全表掃描的順序,遍歷的記錄先上鎖,上鎖以後發現不知足條件,則釋放鎖,而後繼續日後遍歷,直到全表掃描結束。
insert 都不會被阻塞。
而 update 其它字段值,其實也是找記錄,若是找到的記錄已經被上鎖了,那麼就會阻塞,若是找到的記錄沒有被鎖則不會被阻塞。
在可重複讀級別下:
若是鎖定的列爲非索引列,加鎖都是加到主鍵索引上的,select ..for update
的加鎖的順序是從前日後全表掃描的順序,遍歷的記錄先上鎖,上鎖以後發現不知足條件,則不會釋放鎖,而後繼續日後遍歷,直到全表掃描結束。
因此只要有一個全表掃描的加鎖,則 insert 的時候就會被阻塞。
而 update 其它字段值,其實也是找記錄,若是找到的記錄已經被上鎖了,那麼就會阻塞,若是找到的記錄沒有被鎖則不會被阻塞。
與之相關的還有一個問題:
圖裏已經有答案了,包括前面的截圖也能夠看到全部的 lock_type 都是 RECORD ,也就是行級鎖。
實驗三:隔離級別爲讀提交,鎖定索引列的實驗
此時在 name 列創建索引。
CREATE TABLE `yes` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(45) DEFAULT NULL, `address` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_name` (`name`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4
一樣準備數據以下:
發起事務 A,執行以下語句,且事務未提交:
接着發起事務 B,執行以下語句:
能夠看到,不會被阻塞,絲滑。
這個結果符合認知,由於此時 name 已經有索引了,在讀提交級別下,只會在 name 索引上加相關記錄的鎖,而不會加全錶行鎖,所以事務 A、B 之間不會被阻塞。
此時再起一個事務 C,執行以下語句:
能夠看到,發生了阻塞,此時查看鎖信息:
能夠看到,鎖的索引確實變成了 idx_name,lock_data 顯示鎖的是 yes 這個記錄,id 爲 1。
從結果看:在能夠命中二級索引的狀況下,鎖的是對應的二級索引。
咱們繼續作實驗。
將上面全部事務提交以後。
啓動事務 C 執行如下語句,且未提交事務:
接着,事務 D 執行如下語句:
並不會發生阻塞,絲滑地插入了數據。
執行 name 同樣的插入,也不會阻塞。
因此在讀提交級別下,對插入都不會產生阻塞。
關於 update 我就不實驗了,和實驗一的差異就是加鎖索引換成了 name 的索引,其餘表現一致。
實驗四:隔離級別爲可重複讀,鎖定索引列的實驗
一樣準備數據以下:
在可重複讀級別下,事務A執行:
接着,事務 B 執行:
此時發生了阻塞,查看鎖信息:
這是預期以內的阻塞,由於按照 name 爲索引,yes這條記錄是排在最後的(字母序),爲了防止幻讀,可重讀隔離級別下會在對應記錄先後加入間隙鎖,而新的記錄的插入恰巧須要排 yes 這條記錄的後面。
可是從截圖結果來看此時lock_mode是記錄鎖,且 lock_data 是 supremum,這又涉及到個人盲區了,難道是最後的記錄插入比較特殊?因此不是由於間隙鎖被阻塞,而是被最大記錄行鎖阻塞?
此時把事務A、B都提交了 ,而後咱們再執行事務 C:
接着再執行事務 D:
此時的插入不會被阻塞,由於事務 C 鎖的是記錄 yes 左右的間隙和 yes 自己,而事務B提交了,所以事務D插入的不是被鎖定的位置。
若是此時事務 C 接着再執行:
則會被阻塞,咱們看下鎖的信息:
能夠看到,此時被阻塞的鎖是記錄鎖+間隙鎖(next-key lock),這符合咱們的認知和上面的圖,由於要插入的數據在 yes 和公衆號:yes的練級攻略之間。
update我就不實驗了,不是全表掃描,只會根據索引加鎖掃描到的記錄。
小結
在命中索引列的前提下,只會在索引列上加鎖。
若是此時在讀已提交級別下:
select..for update和update
的所查找的記錄自己會被加上記錄鎖,所以這個位置的插入會被阻塞,其餘位置的插入則沒有影響。
若是此時在可重複讀級別下:
select..for update和update
的所查找的記錄在索引位置先後會被加間隙鎖,記錄自己加記錄鎖,所以這些位置的插入會被阻塞,其餘位置的插入則沒有影響。
最後
分了四個實驗大類,一個作了十三個實驗。
仍是挺有收穫的,驚喜就是發現了細節盲區,以後研究一下再出一篇文章。
從實驗來看,這裏再作個概念性的總結:
-
鎖是做用在索引上的,所以若是能命中二級索引就在二級索引上加鎖,否則就得 被迫在聚簇索引上加鎖。
-
被迫在聚簇索引上加鎖,會致使全表掃描式的加鎖。
-
在可重複讀下,不論命中哪一個索引,不管是select..for update仍是update,只要被掃描到的記錄,都會被加鎖,不管是否符合條件,在事務提交以後纔會釋放。
-
在讀提交下,select..for update表現出來的結果是掃描到的記錄先加鎖,再判斷條件,不符合就立馬釋放,不須要等到事務提交,而 update 的掃描是先判斷是否符合條件,符合了才上鎖。
聲明:以上實驗是基於 MySQL 5.7.26 版本,存儲引擎爲 InnoDB 。
這些實驗我以前花了三個工做日晚上作的,因爲時間是零散的,致使中間實驗出錯,期間設置事務隔離級別語句有問題,致使我在錯誤的前提下作實驗,實驗結果不斷地衝擊個人認知,我整我的都快搞崩潰了....
而後週六花了一天的時間從新理了一下,實驗圖不少,可能看了後面就忘了前面,建議結合着結論來回看,這樣對結論會有更深入的認識,可是有些實驗結論我是根據實驗現象來推斷的,我沒有去找相關的官網說明,若有錯誤,懇請指正,若有疑惑還請自行實驗,能夠在評論區交流一番。
推薦閱讀:
歡迎關注個人公衆號【yes的練級攻略】,更多硬核文章等你來讀。
我是yes,從一點點到億點點,咱們下篇見~
本文分享自微信公衆號 - yes的練級攻略(yes_java)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。