昨天一個項目的生產環境出現了數據庫死鎖問題,導致死鎖的訂單號已經提交給第三方,但是由於出錯回滾到該訂單號未記錄的狀態,結果后續的單子使用的單號仍以該單號開始,這在第三方看來不是新單,而是舊單重復調用接口,就報權限錯誤;即后續所有的新單都卡在這里。這是一個很嚴重的bug,自增單號的邏輯顯然有問題,但是這里看一下死鎖問題。
查看死鎖日志后,發現涉及到死鎖的應該是兩張表,表1是關聯表,一般根據order_id和customer_id查詢
CREATE TABLE `rel_credit_customer_files` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id', `order_id` varchar(32) DEFAULT NULL COMMENT '', `customer_id` varchar(32) DEFAULT NULL COMMENT '', `file_id` varchar(32) DEFAULT NULL COMMENT '',PRIMARY KEY (`id`), KEY `pk_order_customer` (`customer_id`,`order_id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=247 DEFAULT CHARSET=utf8mb4 COMMENT='客戶圖片關聯表啊';
表2是第三方訂單信息表,order_id是本家訂單號,vx_order_id是第三方訂單號。
CREATE TABLE `biz_credit_order_info` ( `order_id` varchar(32) NOT NULL COMMENT ' 訂單號', `vx_order_id` varchar(50) DEFAULT NULL COMMENT '第三方返回的訂單號', `withdraw_time` datetime DEFAULT NULL COMMENT '退回時間',PRIMARY KEY (`order_id`) USING BTREE, KEY `pk_order_id` (`order_id`) USING BTREE, KEY `pk_bank` (`bank`), KEY `pk_institution` (`institution_id`), KEY `bank_code` (`bank`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='征信訂單信息表';
涉及到的語句很多,查看死鎖日志后鎖定了關鍵的語句,事務1
replace into biz_credit_order_info(order_id,vx_order_id,withdraw_time) values('2020xxx20','vx001011003753200319030116352','2020-09-09 09:09:09'); delete from rel_credit_customer_files where order_id=' 2020xxx20 ' and customer_id=20; insert into rel_credit_customer_files( customer_id, file_id, order_id, customer_name ) values(20,1,'2020xxx20','張三');
事務2
delete from rel_credit_customer_files where order_id=' 2020xxx08 ' and customer_id=8; insert into rel_credit_customer_files( customer_id, file_id, order_id, customer_name ) values(8,2,'2020xxx08','張三'); update biz_credit_order_info vx_order_id = 'vx001011003753187396677275648', withdraw_time ='2020-09-09 10:25:29' where order_id = '2020xxx08' or vx_order_id = 'vx001011003753187396677275648';
這兩個事務是由兩個不同的方法執行的(新增和更新),由於疏忽,語句的執行順序反了,這就為死鎖埋了禍根。
rel_credit_customer_files表的兩條語句只用看刪除的語句,忽略其新增語句(新增語句對死鎖形成原因沒有影響)
事務1 |
事務2 |
begin; | begin; |
replace into biz_credit_order_info(order_id,vx_order_id,withdraw_time) values('2020xxx20','vx001011003753200319030116352','2020-09-09 09:09:09'); |
|
delete from rel_credit_customer_files where order_id=' 2020xxx08 ' and customer_id=8;(Affected rows: 2) |
|
delete from rel_credit_customer_files where order_id=' 2020xxx20 ' and customer_id=20;(被阻塞) |
|
(Deadlock found when trying to get lock; try restarting transaction) | update biz_credit_order_info vx_order_id = 'vx001011003753187396677275648', withdraw_time ='2020-09-09 10:25:29' where order_id = '2020xxx08' or vx_order_id = 'vx001011003753187396677275648'; Affected rows: 0 ) |
問題在於sql語句寫的不嚴謹導致改語句出現了鎖表情況,第一步應該加了next_key鎖。第二步的刪除語句:本意是根據非唯一索引刪除,但是customer_id 為int值與類型不符,發生類型轉化,原本走索引變成全表掃描,結果鎖所有記錄(日志中持有136條記錄鎖)。第三步的刪除操作申請所有記錄的鎖被阻塞。第四步更新操作有 or操作,正好vx_order_id沒有索引,會進行全表掃描(explain它還是會走主鍵索引),會申請第一步中的記錄鎖而阻塞,於是發生死鎖。發生死鎖的刪除語句和更新語句雖然不是操作同一條記錄,但沒有走索引導致可能鎖所有行,而兩個事務的sql執行順序也是相反的,導致互相等待發生死鎖。以上死鎖情況如果是事務2先執行第一步同樣會死鎖。
將刪除語句的數據類型變更/更新語句的vx_order_id字段加索引/更改語句執行順序,均可以避免死鎖。死鎖日志提供的信息有限,還需要根據具體的sql執行以及分析語句的索引使用情況信息,才好找出問題。