使用select for share,for update的場景及死鎖陷阱


SELECT ... FOR SHARE 和 SELECT ... FOR UPDATE語句是innodb事務中的常用語句
for share會給表增加一個is鎖,給記錄行增加一個s鎖,for update會給表增加一個ix鎖,給記錄行增加一個x鎖。

SELECT ... FOR SHARE使用場景

他們的意思就如語法表示的一樣,SELECT ... FOR SHARE,我選擇一些記錄,這些記錄可以share,其他事務也可以讀,但是如果你要修改,不好意思,我加了一個s鎖,你是不可以修改的。這個語句的應用場景之一是用來讀取到最新的數據。
例如,因為innodb中mvcc機制的存在,在可重復讀隔離級別下,A事務修改某一行的數據,B事務在A事務提交前是看不到A事務對該行的修改的,但是利用SELECT ... FOR SHARE,B事務會等待A事務釋放該行的鎖才能查看到該行數據。
創建一個測試表:

-- ----------------------------
-- Table structure for test_tab
-- ----------------------------
DROP TABLE IF EXISTS `test_tab`;
CREATE TABLE `test_tab` (
`f1` int(11) NOT NULL AUTO_INCREMENT,
`f2` varchar(11) NOT NULL DEFAULT '1',
PRIMARY KEY (`f1`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- ----------------------------
-- Records of test_tab
-- ----------------------------
INSERT INTO `test_tab` VALUES ('1', '1');

 

 

SELECT ... FOR UPDATE使用場景

下面再說SELECT ... FOR UPDATE,我選擇一些記錄,這些select的記錄是我下一步要update的,你要讀或者修改這些記錄,不好意思,我加的是x鎖,你讀不了也改不了。只有我當前事務提交了,這些記錄你才可以讀到或者修改。這個語句的應用場景之一是為了防止更新丟失。
例如,A事務和B事務同時讀取銀行賬戶余額,是2元錢,A事務看到2元,消費了1元,將余額更新為1元,B事務看到2元,消費了1元,也將余額更新為1元,那么賬戶變為1元,但是實際應該扣費2元。使用SELECT ... FOR UPDATE讀取記錄,可以避免這種丟失更新的現象
丟失更新現象:

 

防止丟失更新

 

可能有人看到這里會有疑問:為什么innodb采用MVCC這種多版本並發控制,每次看到的不是最新的數據,而是以前的一個快照呢?
這是因為一個事務的操作有可能成功,也有可能失敗rollback,在一個事務commit之前,被其他事務讀到還沒提交的變更記錄,會產生數據不一樣的現象(臟讀),這種情況就是innodb最低的隔離級別READ UNCOMMITTED,可以讀到沒有commit的數據。
那么如果想要不產生臟讀,容易想到的是采用鎖的方式,當一個事務更改某行記錄,就加上鎖,其他事務等待該事務執行完畢才能讀取到該行記錄,但是這樣做的話會產生大量的鎖占用與等待,效率是非常低下的,因此innoDB采用了MVCC的方式。簡單的說,A事務變更某行記錄,innodb會產生對應的redo log,如果接下來A事務進行回滾,innodb可以根據redo log將記錄回滾到事務開始之前的狀態。在A事務沒有結束時,如果B事務來查詢該行記錄,B事務會根據A事務變更后的記錄值(在內存中)加上redo log“計算”出A事務開始前的該行記錄值,從而讀取到該行記錄的一個快照,其中並不會產生鎖與等待。
如果是可重復讀REPEATABLE READ的隔離級別(默認隔離級別),B事務進行過程中看到的始終會是B事務開始前的記錄行快照信息,不管B事務進行過程中A事務有沒有完成;如果是提交讀READ COMMITTED級別,B事務進行過程中,可以看到A事務提交對記錄行修改值(即如果A事務沒有完成,B查詢到的是A事務開始前的記錄值,如果A事務完成了,B事務查詢到的是A事務完成后的記錄值),在這種情況下會產生不可重復讀的現象,即同一次事務中多次查詢看到的結果會不一樣。

使用select for share,for update的陷阱

再說使用select for share,for update的陷阱,for share會給記錄行增加一個s鎖,for update會給記錄行增加一個x鎖。如果此時有另一個事務B也想給這些記錄行加s鎖或者x鎖,此時就會產生等待,即事務B等待事務A,此時,如果事務A對這些記錄行想加上另一個類型的鎖,就會產生死鎖,用等待圖來表示就是,事務B在等待事務A釋放資源,接下來,事務A又必須等待事務B釋放資源,如此形成了一個有向的環。讓我們舉例說明,為了方便觀察,我們將鎖等待超時時間設置長一點,首先,來看一個互相占用資源的例子:

-- ----------------------------
-- Table structure for test_tab
-- ----------------------------
DROP TABLE IF EXISTS `test_tab`;
CREATE TABLE `test_tab` (
`f1` int(11) NOT NULL AUTO_INCREMENT,
`f2` varchar(11) NOT NULL DEFAULT '1',
PRIMARY KEY (`f1`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- ----------------------------
-- Records of test_tab
-- ----------------------------
INSERT INTO `test_tab` VALUES ('1', '1');
INSERT INTO `test_tab` VALUES ('2', '1');

 

死鎖示例1:

 

上面的示例中,A等B,只要B釋放資源,A就可以進行下去,但是B接下來的操作是去等待A,形成了一個環,產生死鎖。
這種互相占有不同資源的例子等待對方釋放應該是最常見的死鎖場景了,下面,我們來看一下不常見的

 

死鎖示例2:

 

-- ----------------------------
-- Table structure for test_tab
-- ----------------------------
DROP TABLE IF EXISTS `test_tab`;
CREATE TABLE `test_tab` (
`f1` int(11) NOT NULL AUTO_INCREMENT,
`f2` varchar(11) NOT NULL DEFAULT '1',
PRIMARY KEY (`f1`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- ----------------------------
-- Records of test_tab
-- ----------------------------
INSERT INTO `test_tab` VALUES ('1', '1');

 

上述兩個事務並沒有互相占有不同資源,B事務甚至沒有實際占有資源,但是也產生了死鎖,原因是在第二步中B事務等待A事務釋放資源,並且B事務要求分配一個x鎖,接下來A事務需要一個f1=1的x鎖,但是此時B事務已經在等待x鎖,A事務只有一個s鎖,並不能升級成x鎖,因此A事務需要等待B。最終形成B等A,A又等B的環狀圖,產生死鎖。
如果第一步中A事務使用的是for update呢?那么這種死鎖情況就不會發生,因為for update語句已經申請到一個x鎖,A事務此時持有x鎖就可以直接在第3步執行刪除操作,並不需要等待B事務的任何資源。

 

死鎖示例3:

下面是一個因插入導致產生的死鎖,數據庫創建及數據同上

 

上面這個例子可以看做innoDB中“幻行”的解決方案,使用for share或者for update語句將鎖定記錄及記錄之間的空白區間,阻止任何其他事務在該區間中插入數據(如果其他事務允許插入,這將導致同一個事務中多次讀取到不一樣的數據,如A事務select,B事務insert提交,A事務select for share,可以讀取到B事務剛剛提交的記錄)

此外,根據測試,在mysql8.0中如果next key鎖區間重合,那么只能第一個事務擁有該區間的鎖,其他事務不是等待該區間的鎖,而是等待該區間第一個數據的鎖,這方面的原因不明。如果再配合max等函數的話,又會出現一些神奇的死鎖現象,例如插入意向鎖的沖突。這些方面估計只有查看innodb的源碼才能知道原因了,這里不深入探究了。

總之,明白死鎖的原因是由於事務之間互相等待對方占有的資源,在等待圖中形成了環即可,分析死鎖有以下方式:
查看當前事務
SELECT * FROM information_schema.INNODB_TRX;
查看當前鎖
SELECT * FROM `performance_schema`.data_locks;
查看當前鎖等待
SELECT * FROM `performance_schema`.data_lock_waits;
分析死鎖日志:
show ENGINE INNODB STATUS;
在日志中搜索“LATEST DETECTED DEADLOCK”

我們看到,使用for update或者for share時有可能發生死鎖情況,雖然死鎖並不可怕,mysql擁有死鎖檢測的機制打破死鎖並且我們可以重新選擇執行該事物,當時當死鎖頻繁出現時,還是應當注意並加以排查的。最好的情況是不出現死鎖,因此如果快照數據滿足要求時,少用for share或者for update語句,雖然有時你看起來只是在一行記錄上加鎖,但是由於間隙鎖和下一個鍵鎖的存在,鎖住的可能不止是一行記錄。

參考資料:mysql8.0官方文檔

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM