MySQL死锁分析
- 2025-05-21 18:32:00
- 丁国栋
- 原创 8
MySQL 死锁分析
使用 show engine innodb status;
发现存在死锁。
------------------------
LATEST DETECTED DEADLOCK
------------------------
2025-05-21 11:23:05 140245465437760
*** (1) TRANSACTION:
TRANSACTION 27804122, ACTIVE 0 sec updating or deleting
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1128, 4 row lock(s), undo log entries 1
MySQL thread id 7980727, OS thread handle 140245264070208, query id 1624996326 10.42.8.24 zenta0Abm1n update
REPLACE INTO `zt_userview` SET `account` = 'zhaixiaojian',`programs` = '',`products` = '',`projects` = '',`sprints` = ''
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 762 page no 52 n bits 80 index PRIMARY of table `zentaopaas`.`zt_userview` trx id 27804122 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 762 page no 52 n bits 80 index PRIMARY of table `zentaopaas`.`zt_userview` trx id 27804122 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) TRANSACTION:
TRANSACTION 27804123, ACTIVE 0 sec updating or deleting
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1128, 4 row lock(s), undo log entries 1
MySQL thread id 7980728, OS thread handle 140245161330240, query id 1624996327 10.42.8.24 zenta0Abm1n update
REPLACE INTO `zt_userview` SET `account` = 'lipengrun',`programs` = '',`products` = '116,191',`projects` = '1304,1653,1357,1361,1351,1369,1368,1306,2104,1307,1370',`sprints` = '1655,1656,2105,2387,2388,2922,4005,4104'
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 762 page no 52 n bits 80 index PRIMARY of table `zentaopaas`.`zt_userview` trx id 27804123 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 762 page no 52 n bits 80 index PRIMARY of table `zentaopaas`.`zt_userview` trx id 27804123 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** WE ROLL BACK TRANSACTION (2)
MySQL InnoDB 死锁原因分析
这个死锁案例涉及两个并发事务都执行 REPLACE INTO
操作导致的死锁。以下是详细分析:
死锁发生过程
-
事务1 (27804122) 和 事务2 (27804123) 几乎同时执行
REPLACE INTO
语句-
事务1更新
account = 'zhaixiaojian'
的记录 -
事务2更新
account = 'lipengrun'
的记录
-
-
锁获取情况:
-
两个事务都持有
lock_mode X
(排他锁) 在supremum
记录上 -
同时都在等待获取
insert intention
(插入意向锁)
-
-
死锁形成:
-
事务1持有X锁,等待插入意向锁
-
事务2持有X锁,等待插入意向锁
-
形成互相等待的循环依赖,导致死锁
-
根本原因
-
REPLACE INTO 的工作机制:
-
实际上是先DELETE(如果记录存在)再INSERT
-
这个操作需要获取多种锁,增加了死锁概率
-
-
supremum 记录锁:
-
supremum是索引中的伪记录,表示比索引中任何实际值都大的值
-
两个事务都锁定了这个伪记录,导致冲突
-
-
插入意向锁冲突:
-
插入意向锁是一种间隙锁,允许多个事务在同一个间隙中插入不冲突的记录
-
但在某些情况下(如REPLACE操作)会产生冲突
-
解决方案建议
-
避免使用 REPLACE INTO:
-
改用
INSERT ... ON DUPLICATE KEY UPDATE
语法 -
这种语法不会先删除记录,减少了锁冲突
-
-
添加事务隔离控制:
START TRANSACTION; INSERT INTO `zt_userview` (...) VALUES (...) AS new ON DUPLICATE KEY UPDATE `programs` = new.`programs`, `products` = new.`products`, `projects` = new.`projects`, `sprints` = new.`sprints`; COMMIT;
-
优化索引设计:
-
确保
account
字段有合适的索引 -
减少锁的范围
-
-
应用层重试机制:
- 捕获死锁异常并自动重试操作
-
批量操作优化:
- 如果可能,合并多个操作为一个批量操作
这种死锁在并发REPLACE操作中较为常见,通过改用ON DUPLICATE KEY UPDATE通常可以显著减少死锁发生概率。
在MySQL中,使用REPLACE INTO ... SET ... 语句有什么风险?
MySQL中REPLACE INTO ... SET ... 语句的风险分析
REPLACE INTO ... SET ... 是MySQL中一种特殊的语法,它结合了INSERT和UPDATE的功能,但使用时存在一些潜在风险:
主要风险
-
自动删除后插入:
-
REPLACE INTO实际上是先删除匹配的行(如果存在),然后插入新行
-
这会导致隐式的DELETE操作,可能触发DELETE触发器
-
-
自增ID问题:
-
如果表有自增主键,替换操作会导致新行获得新的自增ID
-
这可能破坏外键关系或导致ID不连续
-
-
性能影响:
-
对于大表,删除和插入操作比单纯更新更消耗资源
-
需要重建索引,影响性能
-
-
并发问题:
-
在高并发环境下,可能导致死锁或竞争条件
-
因为实际执行的是DELETE+INSERT而非原子操作
-
-
触发器执行:
-
会触发DELETE和INSERT触发器,而不仅仅是UPDATE触发器
-
可能导致意外的副作用
-
-
数据丢失风险:
- 如果SET子句不包含所有列值,未指定的列将被设置为默认值(可能丢失原有数据)
替代方案建议
-
使用INSERT ... ON DUPLICATE KEY UPDATE:
-
更高效,只更新冲突的记录
-
不会删除原有记录
-
-
明确使用UPDATE:
- 当确定记录存在时,直接使用UPDATE更安全
-
事务控制:
- 如果需要原子性操作,考虑使用显式事务
使用场景
REPLACE INTO最适合用于:
-
简单的键值存储
-
不关心自增ID变化的场景
-
需要完全替换整行数据的场合
在大多数现代应用中,ON DUPLICATE KEY UPDATE通常是更好的选择。
假设表的结构是
CREATE TABLE `zt_userview` (
`id` mediumint unsigned NOT NULL AUTO_INCREMENT,
`account` char(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`programs` longtext COLLATE utf8mb4_general_ci,
`sprints` longtext COLLATE utf8mb4_general_ci,
`products` longtext COLLATE utf8mb4_general_ci,
`projects` longtext COLLATE utf8mb4_general_ci,
PRIMARY KEY (`id`),
UNIQUE KEY `account` (`account`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci 。
使用 INSERT INTO ... ON DUPLICATE KEY UPDATE
改写 SQL 语句 REPLACE INTO `zt_userview` SET `account` = 'lipengrun',`programs` = '',`products` = '116,191',`projects` = '1304,1653,1357,1361,1351,1369,1368,1306,2104,1307,1370',`sprints` = '1655,1656,2105,2387,2388,2922,4005,4104'
INSERT INTO `zt_userview`
(`account`, `programs`, `products`, `projects`, `sprints`)
VALUES
('dingguodong', '', '116,191', '1304,1653,1357,1361,1351,1369,1368,1306,2104,1307,1370', '1655,1656,2105,2387,2388,2922,4005,4104')
AS new
ON DUPLICATE KEY UPDATE
`programs` = new.`programs`,
`products` = new.`products`,
`projects` = new.`projects`,
`sprints` = new.`sprints`;
或者先查询account是否存在,如果不存在则插入,如果存在则更新。
注意:在选择某个方案时需要尽可能选择开销小的。比如“先查询account是否存在,如果不存在则插入,如果存在则更新” 和 “先按照account删除,再插入”,后者开销更大些。
InnoDB作为事务型存储引擎,必须保证ACID特性:
-
原子性 (Atomicity): 每个操作要么完全执行,要么完全不执行
-
一致性 (Consistency): 数据库从一个一致状态变为另一个一致状态
-
隔离性 (Isolation): 并发事务互不干扰
-
持久性 (Durability): 提交的事务永久保存
即使简单查询也需要这些保障,因此必须放在事务中执行,所以InnoDB引擎的每一个查询暗含事务,每个查询都是事务的。