MySql 更新死鎖問題 Deadlock found when trying to get lock; try restarting transaction

MySql 更新死鎖問題 Deadlock found when trying to get lock; try restarting transaction

1.場景

//table1
CREATE TABLE `retailtrades` (
  `TradeId` bigint(20) NOT NULL COMMENT '主鍵',
  `TradeCode` varchar(20) NOT NULL COMMENT '交易單號',
  `TradeAmount` decimal(14,4) NOT NULL COMMENT '交易金額',
  `CreateTime` datetime NOT NULL COMMENT '創建時間',
  `TradeState` tinyint(4) NOT NULL COMMENT '交易狀態 1:未支付 2:已支付 3:支付異常',
  `SuccessTime` datetime DEFAULT NULL COMMENT '支付成功時間',
  PRIMARY KEY (`TradeId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='零售交易單表';

//table2
CREATE TABLE `retailorders` (
  `OrderId` bigint(20) NOT NULL COMMENT '主鍵',
  `OrderCode` varchar(20) NOT NULL COMMENT '訂單編號',
  `CreateTime` datetime NOT NULL COMMENT '創建時間',
  `OrderStatus` tinyint(4) NOT NULL COMMENT '1:待付款 2:待確認 3:待發貨 4:待收貨 5:已完成 6:已取消',
  PRIMARY KEY (`OrderId`),
  UNIQUE KEY `OrderCode` (`OrderCode`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='零售訂單表';


//sql1
update retailtrades set TradeState=2 where TradeCode='111706022040540002';

//sql2
update retailorders set OrderStatus=2 where FIND_IN_SET(ordercode,'111706022040540001'); 

//提交方式:組裝到hashtable內事務提交

//當併發請求時出現問題:
Deadlock found when trying to get lock; try restarting transaction

2.知識點

  1. mysql innodb引擎支持事務,更新時採用的是行級鎖。
  2. 行級鎖必須建立在索引的基礎
  3. 行級鎖並不是直接鎖記錄,而是鎖索引,如果一條SQL語句用到了主鍵索引,mysql會鎖住主鍵索引;如果一條語句操作了非主鍵索引,mysql會先鎖住非主鍵索引,再鎖定主鍵索引。如果操作用到了主鍵索引會先在主鍵索引上加鎖,然後在其他索引上加鎖,否則加鎖順序相反。
  4. 對於沒有用索引的操作會採用表級鎖
  5. mysql FIND_IN_SET 函數會全表掃描

3.問題分析

  1. table1 TradeCode字段未設置索引 update語句會鎖表
  2. table2 OrderCode字段雖設置索引但使用FIND_IN_SET 作爲查詢條件 也會鎖表
  3. 程序採用hashtable組裝sql語句,由於hash執行是無序的,若同時併發兩個請求事務執行順序如下:
事務1 事務2
begin TRANSACTION ...
update retailtrades set TradeState=2 where TradeCode='111706022040540002'; begin TRANSACTION
鎖住table1等待table2 update retailorders set orderstatus=2 where FIND_IN_SET(ordercode,'111706030019320003');
... 鎖住table2等待table1
update retailorders set orderstatus=2 where FIND_IN_SET(ordercode,'111706022040540001'); UPDATE retailtrades set tradestate=2 where tradecode='111706030019320004';
死鎖 事務1死鎖處理後,事務2獲取鎖執行成功

4.問題模擬重現

//sql1
start TRANSACTION;
UPDATE retailtrades set tradestate=2 where tradecode='111706022040540002';
select sleep(5);
update retailorders set orderstatus=2 where FIND_IN_SET(ordercode,'111706022040540001');
COMMIT

//sql2 
start TRANSACTION ;
update retailorders set orderstatus=2 where FIND_IN_SET(ordercode,'111706030019320003');
SELECT sleep(5);
UPDATE retailtrades set tradestate=2 where tradecode='111706030019320004';
COMMIT

//於navicat中打開兩個窗口先執行sql1,後執行sql2,查看執行結果

5.問題解決

通過以上分析結果問題最簡單暴力的解決方式就是講hashtable組裝sql改爲有序集合,但此種解決方式並不能解決以上sql與表的性能問題。
因此建議如下:

  1. table1 TradeCode字段 考慮是否增加索引提高查詢更新速度,避免更新鎖表
  2. sql2 避免使用FIND_IN_SET(注意:FIND_IN_SET會全表掃描,效率低下,查詢的時候儘量也不要使用),可改爲in
  3. sql組裝方式改爲有序集合
  4. 建議update語句使用主鍵索引作爲更新條件

6.問題擴展

CREATE TABLE `user_item` (
  `id` BIGINT(20) NOT NULL,
  `user_id` BIGINT(20) NOT NULL,
  `item_id` BIGINT(20) NOT NULL,
  `status` TINYINT(4) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_1` (`user_id`,`item_id`,`status`)
) ENGINE=INNODB DEFAULT CHARSET=utf-8
update user_item set status=1 where user_id=? and item_id=?
  1. 由於用到了非主鍵索引,首先需要獲取idx_1上的行級鎖
  2. 緊接着根據主鍵進行更新,所以需要獲取主鍵上的行級鎖;
  3. 更新完畢後,提交,並釋放所有鎖。
//如果在步驟1和2之間突然插入一條語句:
update user_item .....where id=? and user_id=?
//這條語句會先鎖住主鍵索引,然後鎖住idx_1。
//蛋疼的情況出現了,一條語句獲取了idx_1上的鎖,等待主鍵索引上的鎖;
//另一條語句獲取了主鍵上的鎖,等待idx_1上的鎖,這樣就出現了死鎖。

解決方案

//1.先獲取需要更新的記錄的主鍵
select id from user_item where user_id=? and item_id=?
//2. 逐條更新
...
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章