mysql死锁报错为“deadlock found when trying to get lock; try restarting transaction”,仅在事务被选为牺牲者且rr隔离级别下触发,由innodb自动检测并回滚,错误码1213,与锁等待超时无关。

MySQL死锁报错怎么识别和区分
MySQL遇到死锁时,客户端收到的错误是 Deadlock found when trying to get lock; try restarting transaction,不是超时或连接中断。这个错误只在事务被选为“牺牲者”时触发,且仅发生在可重复读(RR)隔离级别下,因为MVCC + next-key lock机制容易形成环路。
- 死锁检测由InnoDB自动完成,耗时极短,但会强制回滚其中一个事务
- 不同于锁等待超时(
Lock wait timeout exceeded),死锁报错不依赖innodb_lock_wait_timeout设置 - 如果应用没捕获
1213错误码(对应死锁),就可能把失败当普通异常吞掉,导致数据不一致
Python中重试逻辑必须避开的三个坑
用try/except包住cursor.execute()再简单sleep后重试,看似合理,实则危险:
-
Connection对象不能复用已回滚事务的状态:一旦触发死锁,当前事务已结束,必须显式调用conn.rollback(),否则下次execute会报Commands out of sync - 重试次数无上限会导致雪崩:建议硬限制3次,第3次失败直接抛出原始异常,避免压垮数据库
- 没有退避策略:固定sleep 0.1秒会让重试集中在同一毫秒窗口,加剧竞争;应改用指数退避,比如
time.sleep(0.05 * (2 ** attempt))
示例关键片段:
for attempt in range(3):
try:
cursor.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
conn.commit()
break
except Exception as e:
if "1213" in str(e): # MySQL死锁错误码
conn.rollback()
time.sleep(0.05 * (2 ** attempt))
else:
raise
Java里用Spring @Transactional怎么安全重试
Spring默认的@Transactional对死锁完全无感知——它不会自动重试,也不会在死锁后清空事务上下文。直接加@Retryable(value = {DeadlockLoserDataAccessException.class})也不行,因为:
-
DeadlockLoserDataAccessException只在JDBC驱动抛出SQLState =40001时才被包装,而MySQL Connector/J 8.0+默认不启用useServerPrepStmts=true时可能不返回标准SQLState - 若方法内含多个
save()调用,重试会重复执行全部操作,而非仅重试失败语句 - 必须配合
@EnableRetry和RetryTemplate手动控制,且事务传播行为得设成REQUIRES_NEW,否则外层事务一滚,重试也失效
更稳妥的做法是把写操作封装进独立方法,并用TransactionTemplate手动控制:
transactionTemplate.execute(status -> {
jdbcTemplate.update("UPDATE orders SET status = ? WHERE id = ?", "shipped", orderId);
return null;
});
哪些操作根本没法靠重试解决
死锁重试只适用于“纯数据变更冲突”,一旦混入以下场景,重试只会让问题更隐蔽:
- 涉及
SELECT ... FOR UPDATE且条件范围动态变化(如分页查询最后一页),每次重试可能锁不同行,死锁模式飘移 - 存储过程中调用
SLEEP()或外部HTTP请求,事务持锁时间不可控,重试放大延迟 - 使用
AUTO_INCREMENT主键+批量插入,InnoDB会在索引间隙加锁,重试可能触发新死锁而非原路径
真正要压降死锁率,得从SQL顺序入手:所有服务统一按user_id升序更新账户,比任何重试都管用。重试只是兜底,不是解药。











