跳转到主内容
新派编程网 - 新潮编程理念,全新技术课堂!

MySQL如何解决频繁的死锁与锁等待_事务粒度缩减与加锁顺序一致

快速定位MySQL死锁根因需执行SHOW ENGINE INNODB STATUS\G,重点分析LATEST DETECTED DEADLOCK区块,明确两个事务各自持有的锁(HELD LOCKS)和等待的锁(WAITING FOR THIS LOCK TO BE GRANTED),结合TABLE、INDEX、lock_mode及space id/page number/record等信息精准定位冲突行与索引。 死锁报错
Deadlock found when trying to get lock
怎么快速定位根因 MySQL 的死锁不是随机发生的,而是事务间加锁顺序冲突的必然结果。关键不是“怎么重试”,而是看清楚哪两个事务、在哪些行、按什么顺序抢了哪些锁。 执行
SHOW ENGINE INNODB STATUS\G
,重点找
LATEST DETECTED DEADLOCK
区块:它会明确列出两个事务各自持有的锁(
HELD LOCKS
)和正在等待的锁(
WAITING FOR THIS LOCK TO BE GRANTED
)。注意里面的
TABLE
、
INDEX
、
lock_mode
(如
X locks rec but not gap
)和具体
space id
/
page number
/
record
—— 这些才是真实冲突点。 别只看 SQL 文本,同一句
UPDATE user SET balance = ? WHERE id = ?
在不同事务里可能锁住不同索引(比如一个走主键,一个走二级索引 + 回表),锁范围完全不同 如果看到
lock_mode X locks gap before rec
,说明有间隙锁参与,大概率是 WHERE 条件用了非唯一索引或范围查询 事务中混用
SELECT ... FOR UPDATE
和普通
UPDATE
时,InnoDB 可能对同一行加两次锁(一次显式,一次隐式),加剧冲突 为什么把大事务拆成小事务后,
Lock wait timeout exceeded
还是频繁出现 拆事务能降低单次持有锁的时间,但解决不了“多个小事务仍按不同顺序访问相同资源”这个本质问题。锁等待多,往往意味着并发路径存在竞争热点,而非单纯事务太长。 典型场景:订单服务和库存服务都按“先查再更新”逻辑操作
product
表,但一个按
id
查,一个按
sku
查——即使各自事务只有两行 SQL,只要访问顺序不一致,就可能形成环形等待。 检查所有涉及同一张表的写操作路径,确保所有业务逻辑都通过**同一个唯一索引字段**定位记录(比如统一用
id
,而不是有时用
id
、有时用
order_no
或
user_id
) 避免在事务内做 RPC 调用或文件 IO,这些会拉长锁持有时间,且无法被 MySQL 监控到
autocommit=0
下手动开启的事务,务必确认每个分支都有
COMMIT
或
ROLLBACK
,漏掉会导致锁一直挂着
SELECT ... FOR UPDATE
加锁范围到底由什么决定 不是由语句长得像不像,而是由执行计划 + 索引结构 + 隔离级别共同决定。同一句 SQL,在不同数据分布、不同索引覆盖下,锁的行数、是否带间隙锁,可能天差地别。 例如:
SELECT * FROM order WHERE status = 'pending' FOR UPDATE
—— 如果
status
没有索引,就会锁全表;如果有索引但不是唯一索引,InnoDB 会在匹配的每条记录上加记录锁,并在它们之间的间隙加间隙锁(防止幻读)。 用
EXPLAIN FORMAT=tree
看实际走哪个索引,是否发生索引下推(ICP)、是否回表 在
READ COMMITTED
隔离级别下,InnoDB 不加间隙锁(只锁匹配到的行),但
REPEATABLE READ
下默认加,这是很多死锁的隐藏来源 想精确控制锁范围?改用
SELECT ... FOR UPDATE OF table_name
(8.0.1+)或更稳妥的方式:先用唯一索引精确查出主键,再用
WHERE id = ? FOR UPDATE
如何让多个服务模块对同一张表的加锁顺序绝对一致 没有魔法配置能让 MySQL 自动排序加锁顺序。唯一可靠的方式,是在应用层强制所有写路径按**同一套规则生成访问序列**,比如按主键升序批量处理。 常见错误是:A 服务按
created_at
倒序取一批订单更新,B 服务按
id
正序取同一批订单更新——哪怕数据集完全一样,加锁顺序也相反,极易死锁。 所有批量更新前,先
SELECT id FROM ... ORDER BY id ASC
拿到有序 ID 列表,再按此顺序逐条或分批
UPDATE ... WHERE id = ?
避免在事务中动态拼接 IN 列表(如
WHERE id IN (3,1,5)
),InnoDB 对 IN 的加锁顺序不保证与列表顺序一致;改用临时表或多次单值更新 微服务间共享数据库时,建议用
SELECT ... FOR UPDATE SKIP LOCKED
(8.0.1+)替代应用层排队,它能跳过已被锁的行,天然规避部分竞争 最麻烦的不是锁本身,而是不同模块对同一张表的访问逻辑散落在十几个服务里,没人知道谁在什么时候、以什么顺序、锁了哪些索引。上线前不梳理清楚访问路径,光调参数或加重试,只是把问题拖得更隐蔽。

相关文章