1、问题
线上出现很多添加 key 锁超时的 mysql 日志。
https://apm.example.com/app/apm/services/<service_name>/transactions/view?rangeFrom=now-24h%2Fh&rangeTo=now&environment=
2、排查
1)查看 APM,发现了一个耗时的请求/sync,做语料同步的。后面插入语料 key 都显Lock wait timeout exceeded。

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

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 值时
插入意向锁冲突:
-
新的插入请求需要获取插入意向锁
-
与长事务持有的间隙锁冲突
-
导致其他插入操作被阻塞