1. 线上事故
昨天晚上下班还在路上的时候就被领导打电话说是线上出故障了,客户的设备列表查询报错,让我紧急排查下,这会我还在地铁上呢,回到家后紧急远程连接公司电脑,开始排查问题。最后搞到了 11:30 才算结束。
2. 原理解释
MySQL 中的死锁通常发生在多个事务在同一组资源上竞争时,导致每个事务等待其他事务释放资源。在这种情况下,如果一个事务执行了COUNT(*)
查询,而另一个执行了TRUNCATE TABLE
,这两个操作可能会因为锁的不兼容性而引起死锁。
COUNT(*)
通常会扫描整个表来计算行数,这可能会加上表级别的共享锁(S锁)。而 TRUNCATE TABLE
会获取更强的锁,如表级别的独占锁(X锁),这会导致死锁,因为 COUNT(*)
持有 S 锁,而TRUNCATE TABLE
需要 X 锁。
3. 事故分析
使用SHOW FULL PROCESSLIST;
查询结果如下:
show processlist 结果字段解释:
Id: 就是这个线程的唯一标识,当我们发现这个线程有问题的时候,可以通过 kill 命令,加上这个 Id 值将这个线程杀掉。show processlist 显示的信息时来自information_schema.processlist 表,所以这个 Id 就是这个表的主键。
User: 就是指启动这个线程的用户。
Host: 记录了发送请求的客户端的 IP 和端口号。通过这些信息在排查问题的时候,我们可以定位到是哪个客户端的哪个进程发送的请求。
db: 当前执行的命令是在哪一个数据库上。如果没有指定数据库,则该值为 NULL 。
Command: 是指此刻该线程正在执行的命令。
Time: 表示该线程处于当前状态的时间。
State: 线程的状态,和 Command 对应。
Info: 一般记录的是线程执行的语句。默认只显示前100个字符,也就是说看到的语句可能是截断了的,要看全部信息,需要使用 show full processlist。
通过查看这些 SQL 语句,发现一个共同点,它们都涉及到了一张表(后面简称 table_x)的查询,那么问题应该很明确了,这个表出问题了,大概率就是出现死锁了。
可以看出第一条 SQL 是一个count(*)
操作,通过排查代码,发现这是一个 Spring Data JPA 分页查询自动生成的 count 查询,涉及到关联表 table_x。
第二条 SQL 是一个 truncate table 语句,通过排查代码,发现这是一个定时任务,会每小时对表 table_x 进行清空操作,然后再批量 insert into 数据。
就是这两个操作导致了表 table_x 产生了死锁,所以后续的关于表 table_x 的查询操作全部都被阻塞了,进而导致线上客户界面出现错误。
由于远程操作,加上运维不愿意配合,这次也没有去查询一下锁状态,最后运维直接进行了数据库重启,然后线上就正常了。表 table_x 的相关查询也正常了。
后面我会去修改下这个老代码,去掉 truncate table 操作,避免再次出现这个问题。
4. 死锁排查
其实出现死锁一般的解决方法是:
使用SHOW ENGINE INNODB STATUS
查看近期死锁日志信息,主要关注日志中的LATEST DETECTED DEADLOCK
部分,来分析死锁的原因,并相应地优化查询或者事务逻辑。死锁记录只记录最近一个死锁信息,若要将每个死锁信息都保存到错误日志,可以启用以下参数:
show variables like 'innodb_print_all_deadlocks';
考虑使用SHOW PROCESSLIST
或者SHOW FULL PROCESSLIST
查询来查看当前正在执行的查询和它们锁定的资源,并手动进行干预,也就是kill
掉对应的线程。
也可以查询当前正在运行的 InnoDB 事务的信息,可以kill
长期占用锁的事务对应的线程id
select * from information_schema.INNODB_TRX;