INSERT ON DUPLICATE UPDATE與死鎖
在MySQL中提供兩種插入更新的方式:REPLACE INTO和INSERT ON DUPLICATE UPDATE,簡化了“存在則更新,不存在則插入”的實現邏輯,但這兩種方式在MySQL內部都被拆分為多個操作步驟且引入GAP鎖來保證數據完整性,因此在高並發情況下極易產生死鎖。
##==================================================##
在MySQL中INSERT ON DUPLICATE UPDATE的加鎖流程:
1. Innodb存儲引擎嘗試INSER INTO操作
2. 如果插入成功,則忽略DUPLICATE UPDATE部分並返回
3. 如果插入失敗,則表明有相同記錄存在,對該記錄加共享鎖(S)
4. 執行DUPLICATE UPDATE語句,對記錄加X鎖,然后更新記錄。
##==================================================##
對於INSERT ON DUPLICATE UPDATE操作,當兩個會話S1和S2使用INSERT ON DUPLICATE UPDATE語句操作相同數據且表中存在相同鍵值記錄時,觸發死鎖場景為:
1. 由於表中已存在重復鍵值的記錄,導致會話先后嘗試INSER失敗
2. 會話S1進入步驟3嘗試獲取記錄的S鎖,該記錄未被其他會話加鎖,獲取S鎖成功。
3. 會話S2進入步驟3嘗試獲取記錄的S鎖,該記錄上被加持S鎖,但由於S鎖與S鎖兼容,獲取S鎖成功
4. 會話S1進入步驟4嘗試獲取記錄的X鎖,由於會話S2對該記錄持有S鎖,S鎖與X鎖不兼容,獲取X鎖失敗,會話S1被阻塞
5. 會話S2進入步驟4嘗試獲取記錄的X鎖,由於會話S1對該記錄持有S鎖,S鎖與X鎖不兼容,獲取X鎖失敗,會話S2被阻塞
6. 會話S2被阻塞后進入死鎖檢查環節,發現阻塞S1->S2和S2->S1形成死鎖環路,觸發死鎖機制強制回滾S1或S2事務。
##==================================================##
在MySQL默認隔離級別REPEATABLE READ下,為避免出現"幻讀"發生,防止其他會話插入相同鍵值的記錄。
對於普通INSERT操作加鎖如下:
1. 對於非唯一索引,需要對新記錄加排他鎖(X),另外對新記錄和新記錄的相鄰記錄的區間加gap鎖。
2. 對於唯一索引,僅需要對新記錄加排他鎖(X),唯一索引特性保證其他會話無法插入相同鍵值。
對於INSERT ON DUPLICATE UPDATE操作,當表中未存在重復鍵值記錄時,加鎖特點如下:
1. 對於唯一索引和非唯一索引,都需要對新記錄加排他鎖(X),另外對新記錄和新記錄的相鄰記錄的區間加gap鎖。
##==================================================##
准備測試數據:
CREATE TABLE `tb2002` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`c1` varchar(20) DEFAULT NULL,
`c2` varchar(20) DEFAULT NULL,
`c3` int(11) DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_c1` (`c1`,`c2`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;
insert into tb2002(c1,c2) values('a','1');
insert into tb2002(c1,c2) values('f','1');
開始測試:
會話1執行:
insert into tb2002(c1,c2) values('d','2') ON DUPLICATE KEY UPDATE c3=c3+1;
執行成功
會話2執行:
insert into tb2002(c1,c2) values('c','1') ON DUPLICATE KEY UPDATE c3=c3+1;
執行被阻塞,等待X+GAP鎖,GAP鎖的行記錄目標是('f','1')
會話1執行:
insert into tb2002(c1,c2) values('d','1') ON DUPLICATE KEY UPDATE c3=c3+1;
觸發死鎖,會話2被回滾,會話1執行成功
##==================================================##
通過上面兩個死鎖案例,我們強烈建議在生產環境中盡量避免使用REPLACE INTO和INSERT INTO ON DUPLICATE UPDATE語句,改用普通INSERT操作,並對INSER操作部分代碼加入異常加查,當INSERT失敗時改為UPDATE操作。