關於MySQL鎖的兩個知識點
- 2019 年 11 月 6 日
- 筆記
MySQL快照讀和當前讀
在MySQL中,讀操作可以分成兩類:快照讀 (snapshot read)與當前讀 (current read)。快照讀,讀取的是記錄的可見版本 (有可能是歷史版本),不用加鎖。當前讀,讀取的是記錄的最新版本,並且,當前讀返回的記錄,都會加上鎖,保證其他事務不會再並發修改這條記錄。
這裡我們首先給出快照讀和當前讀的例子:
快照讀:簡單的select操作,屬於快照讀,不加鎖。(當然,也有例外,下面會分析)
select * from table where id>10;
當前讀:特殊的讀操作,插入/更新/刪除操作,屬於當前讀,需要加鎖。
select * from table where id>10 lock in share mode; select * from table where id>10 for update; insert into table values (…); update table set id=11 where id=10; delete from table where id>10;
讀取之後,需要保證其他並發事務不能修改當前記錄,對讀取記錄加鎖。其中,除了第一條語句明確指出了lock in share mode之外,也就是對讀取記錄加S鎖 (共享鎖)外,其他的操作,都加的是X鎖 (排它鎖)。
這裡我們給出一個update操作過程中,mysql server和innodb存儲引擎進行交互的過程如下:

從上圖中,我們可以看出一個update操作的具體流程。當update SQL被發給MySQL後,MySQL Server會根據where條件,讀取第一條滿足條件的記錄,然後InnoDB引擎會將第一條記錄返回,並加鎖 (current read)。待MySQL Server收到這條加鎖的記錄之後,會再發起一個update請求,更新這條記錄。一條記錄操作完成,再讀取下一條記錄,直至沒有滿足條件的記錄為止。因此,update操作內部,就包含了一個當前讀。同理,delete操作也一樣。insert操作會稍微有些不同,簡單來說,就是insert操作可能會觸發Unique Key的衝突檢查,也會進行一個當前讀。
2
關於死鎖
死鎖是指兩個或者兩個以上的事務在執行的過程中,因爭奪資源而造成的一種互相等待的現象。若無外力作用,這兩個事務將保持等待狀態,無法推進下去。很明顯,這是我們不想看到的。
從上面的概念可以看出,死鎖的關鍵點在於互相等待,如果我們要解決死鎖的問題,就要從「等待」這個關鍵詞上面入手,如果我們將等待都轉化為回滾操作,並且事務都重新開始,這種方法無疑可以避免死鎖問題的產生。但是會導致數據庫並發性能的降低,這樣的問題也是我們無法接受的。
為了解決這一問題,我們採用一種超時的方法進行折中進行處理,超時是指當兩個事務互相等待時,當某一方的等待時間超過一個閾值,我們將它進行回滾,這樣,另一個事務就能夠繼續進行,在innodb存儲引擎中,我們使用參數innodb_lock_wait_timeout來設置超時時間,這個參數如下:
mysql> show variables like "innodb_lock_wait_timeout"; +--------------------------+-------+ | Variable_name | Value | +--------------------------+-------+ | innodb_lock_wait_timeout | 50 | +--------------------------+-------+ 1 row in set, 1 warning (0.11 sec)
為了加深印象,我們模擬一個死鎖的現象,讓大家感受一下。
首先,要模擬死鎖,程序必須並發運行,串行的方法是無法模擬死鎖的,這裡我們採用兩個連接會話進行模擬:
會話A
我們先開啟事務,然後鎖定id=3的行;
mysql> select * from t; +----+-----+ | id | age | +----+-----+ | 1 | 5 | | 2 | 4 | | 3 | 3 | | 4 | 2 | | 5 | 1 | +----+-----+ 5 rows in set (0.00 sec) mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from t where id=3 for update; +----+-----+ | id | age | +----+-----+ | 3 | 3 | +----+-----+ 1 row in set (0.02 sec)
會話B
在會話B上鎖定id=2的行
mysql> begin -> ; Query OK, 0 rows affected (0.00 sec) mysql> select * from t where id=2 for update; +----+-----+ | id | age | +----+-----+ | 2 | 4 | +----+-----+ 1 row in set (0.00 sec)
會話A
我們在會話A上獲取id=2的記錄的鎖,發現無法獲取,產生了等待:
mysql> select * from t where id=2 for update; ##產生等待 mysql>
會話B
在會話A進行等待的過程中,我們在會話B上面獲取id=3的記錄的鎖,我們發現了兩個變化:
第一、會話B上輸出了死鎖的提示信息,如下;
mysql> select * from t where id=3 for update; ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting traction mysql>
第二、會話A上輸出了id=2的記錄,也就是A會話得到了特定的資源,但是產生了9s的延遲,如下;
會話A
mysql> select * from t where id=2 for update; +----+-----+ | id | age | +----+-----+ | 2 | 4 | +----+-----+ 1 row in set (9.04 sec) mysql>
在上述操作中,會話B拋出了1213這個錯誤狀態碼,它代表事務發生了死鎖,死鎖的原因是會話A和B的資源進行了相互等待,但是此時我們發現會話B中拋出死鎖提示信息之後會話A中立即得到了記錄為2的這個資源,這其實是因為會話B中的事務發生了回滾,否則的話,會話A中的事務是不可能得到相應的資源的。
這裡又不得不提innodb的一個特性,那就是它會回滾死鎖情況下的一個事務,因此當我們在程序中捕獲了一個1213的錯誤,其實不需要我們手動進行回滾。