事务提交时提示 Deadlock found when trying to get lock
怎么办
MySQL 在高并发下出现死锁是常态,不是配置错误或代码 bug 的直接证据。InnoDB 会主动检测并回滚其中一方事务,抛出这个错误——关键不是“避免死锁”,而是“快速重试 + 减少冲突面”。
应用层必须捕获1213错误码(
ER_LOCK_DEADLOCK),不能当作普通异常吞掉或记录后忽略 重试逻辑建议限制在 3 次以内,每次间隔用指数退避(如 10ms → 30ms → 90ms),避免雪崩式重试 检查 SQL 是否存在“多表交叉更新顺序不一致”:比如事务 A 先更新
users再更新
orders,事务 B 反过来,就极易死锁 把
SELECT ... FOR UPDATE或
UPDATE涉及的行尽量按主键升序锁定,可用
ORDER BY id ASC+
LIMIT控制范围
Lock wait timeout exceeded
超时常见原因和调优点
这不是死锁,而是某条语句等锁太久(默认 50 秒)被强制中断,错误码
1205。本质是锁持有时间过长,或等待队列太深。 查当前长事务:
SELECT trx_id, trx_started, trx_state, trx_weight, trx_query FROM information_schema.INNODB_TRX ORDER BY trx_started LIMIT 5;确认是否有未提交的
BEGIN后忘记
COMMIT或
ROLLBACK,尤其注意 ORM 中手动开启事务但异常分支遗漏回滚 避免在事务中做耗时操作(如 HTTP 请求、文件读写、复杂计算),应拆到事务外 临时调大
innodb_lock_wait_timeout是饮鸩止渴;更有效的是缩小事务粒度,例如把“批量更新 1000 行”拆成每 100 行一个事务
如何让 MySQL 自动重试失败事务(不依赖应用代码)
MySQL 本身不提供自动重试机制,但可通过存储过程封装基础逻辑,把重试控制权留在数据库侧。适用于简单、固定模式的写操作(如计数器更新、状态流转)。
用DECLARE EXIT HANDLER FOR 1213, 1205捕获死锁与锁超时 配合
REPEAT ... UNTIL实现最多 3 次尝试,每次
ROLLBACK后
DO SLEEP(0.01)避免忙等 注意:存储过程中不能嵌套事务,需确保外部没开启事务(否则
START TRANSACTION会报错) 示例片段(简化版):
DELIMITER $$
CREATE PROCEDURE safe_update_counter(IN p_id INT)
BEGIN
DECLARE retry_count INT DEFAULT 0;
DECLARE max_retries INT DEFAULT 3;
DECLARE CONTINUE HANDLER FOR 1213, 1205
BEGIN
SET retry_count = retry_count + 1;
IF retry_count <= max_retries THEN
DO SLEEP(0.01);
ITERATE retry_loop;
END IF;
END;
retry_loop: REPEAT
START TRANSACTION;
UPDATE counters SET value = value + 1 WHERE id = p_id;
COMMIT;
LEAVE retry_loop;
UNTIL retry_count > max_retries END REPEAT;
END$$
DELIMITER ;
使用 INSERT ... ON DUPLICATE KEY UPDATE
替代先查后更,减少锁竞争
这是最常被忽视的并发优化手段。很多业务写法是:
SELECT ...判断是否存在 → 不存在则
INSERT,存在则
UPDATE。这中间存在竞态窗口,且两次语句都加锁。
ON DUPLICATE KEY UPDATE是原子操作,只加一次锁,天然规避“查-改”间隙 要求目标字段有
UNIQUE或
PRIMARY KEY约束,否则无法触发冲突逻辑 注意
LAST_INSERT_ID()在冲突时仍会返回插入行的 ID(不是更新行),若业务依赖此值需额外判断
ROW_COUNT()不要滥用:如果更新逻辑复杂(比如要基于旧值做条件计算),仍需显式事务控制,但可考虑用
SELECT ... FOR UPDATE加锁单行再更新,比全表扫描安全
真正难处理的从来不是报什么错,而是事务边界是否清晰、SQL 是否可预测、重试是否幂等。线上一旦出现批量提交失败,优先看
INNODB_TRX和慢查询日志里锁等待链,而不是立刻调参数。
