文章導航-readme
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.知識點
- mysql innodb引擎支持事務,更新時采用的是行級鎖。
- 行級鎖必須建立在索引的基礎
- 行級鎖並不是直接鎖記錄,而是鎖索引,如果一條SQL語句用到了主鍵索引,mysql會鎖住主鍵索引;如果一條語句操作了非主鍵索引,mysql會先鎖住非主鍵索引,再鎖定主鍵索引。如果操作用到了主鍵索引會先在主鍵索引上加鎖,然后在其他索引上加鎖,否則加鎖順序相反。
- 對於沒有用索引的操作會采用表級鎖
- mysql FIND_IN_SET 函數會全表掃描
3.問題分析
- table1 TradeCode字段未設置索引 update語句會鎖表
- table2 OrderCode字段雖設置索引但使用FIND_IN_SET 作為查詢條件 也會鎖表
- 程序采用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與表的性能問題。
因此建議如下:
- table1 TradeCode字段 考慮是否增加索引提高查詢更新速度,避免更新鎖表
- sql2 避免使用FIND_IN_SET(注意:FIND_IN_SET會全表掃描,效率低下,查詢的時候盡量也不要使用),可改為in
- sql組裝方式改為有序集合
- 建議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=?
- 由於用到了非主鍵索引,首先需要獲取idx_1上的行級鎖
- 緊接着根據主鍵進行更新,所以需要獲取主鍵上的行級鎖;
- 更新完畢后,提交,並釋放所有鎖。
//如果在步驟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. 逐條更新
...