1、问题

线上出现很多添加 key 锁超时的 mysql 日志。

https://apm.example.com/app/apm/services/<service_name>/transactions/view?rangeFrom=now-24h%2Fh&rangeTo=now&environment=&transactionName=%23

2、排查

1)查看 APM,发现了一个耗时的请求/sync,做语料同步的。后面插入语料 key 都显Lock wait timeout exceeded。

image-20250627101231500

2)查看同步请求的具体链路,发现有多次批量 INSERT 操作,而且每次批量插入消耗的时间会逐步增加。最终也是执行成功的,花了24 分钟。后面其他事务的 Insert 操作都给阻塞住了。

image-20250627102956158

3)查看锁日志,可以发现并不是死锁,而是普通的锁等待,最开始的批量操作(/sync)占用了大量的锁 4253982 row locks。后面的insert(添加 key)申请插入意向锁都被阻塞,等待一段时间之后上报超时错误给应用。

LOCK WAIT 8 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id <tid_1>, OS thread handle <handle_1>, query id <qid_1> <private-ip> <db_name> update
INSERT INTO multilingual_item (item_id, corpus_id, lang, customize,`key`,value, description)
VALUES (1001, 2001, 'xx-XX', 'default', 'key_1', '<text>', '');
------- TRX HAS BEEN WAITING 22 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 345 page no 83690 n bits 136 index PRIMARY of table `<db_name>`.`multilingual_item` trx id <trx_waiting> 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;;

------------------
---TRANSACTION <trx_long>, ACTIVE 1166 sec starting index read
mysql tables in use 1, locked 1
102372 lock struct(s), heap size 10379472, 4253982 row lock(s), undo log entries 943
MySQL thread id <tid_long>, OS thread handle <handle_long>, query id <qid_long> <private-ip> <db_name> update
INSERT INTO multilingual_item (item_id, corpus_id, lang, customize,`key`,value, status, type) VALUES
    (2001, 3001, 'xx-XX', 'default', 'k1', 'v1', 2, 1),
    (2002, 3001, 'xx-XX', 'default', 'k2', 'v2', 2, 1);

4)查看代码,发现了调用 importJsonCorpus 进行多次批量操作,这跟 APM 看到的现象一致。这个方法上有事务注解,importJsonCorpus 方法的调用都在一个事务里,所以一次批量操作结束之后并不会释放锁。

3、总结

这是一个长事务导致阻塞等待问题,导致其他插入请求超时,并非死锁。

4、解决方案

方法一:大事务拆成小事务,缩小事务范围,每个语言一个事务。

方法二:优化代码,把所有语言合并成一个大的 INSERT … ON DUPLICATE KEY UPDATE语句,相当于导入Excel 语料。

选择:鉴于改动范围和风险,优先选择方法一,如果部分语言同步失败,保证幂等的情况下也可以重复导入。

5、深入解析

问题分析:

  • @Transactional 注解覆盖整个sync方法

  • 当处理完一个语言的批量操作时,并没有释放锁,然后继续下一个

1)执行 sync 方法时,在 uniq_corpus_id_lang_customize_key 索引上的锁。INSERT … ON DUPLICATE KEY UPDATE 语句需要唯一索引约束,所以有了联合唯一索引uniq_corpus_id_lang_customize_key

唯一索引锁:

  • 在 uniq_corpus_id_lang_customize_key 索引上的锁

  • 包括:间隙锁(Gap Lock)+ 记录锁(Record Lock)

  • 防止并发插入相同的唯一键值

2)执行创建获修改key 值时

插入意向锁冲突:

  • 新的插入请求需要获取插入意向锁

  • 与长事务持有的间隙锁冲突

  • 导致其他插入操作被阻塞