COUNT(*) 和 TRUNCATE TABLE 产生死锁

2024年 7月 24日 67.5k 0

1. 线上事故

昨天晚上下班还在路上的时候就被领导打电话说是线上出故障了,客户的设备列表查询报错,让我紧急排查下,这会我还在地铁上呢,回到家后紧急远程连接公司电脑,开始排查问题。最后搞到了 11:30 才算结束。

2. 原理解释

MySQL 中的死锁通常发生在多个事务在同一组资源上竞争时,导致每个事务等待其他事务释放资源。在这种情况下,如果一个事务执行了COUNT(*)查询,而另一个执行了TRUNCATE TABLE,这两个操作可能会因为锁的不兼容性而引起死锁。

COUNT(*)通常会扫描整个表来计算行数,这可能会加上表级别的共享锁(S锁)。而 TRUNCATE TABLE会获取更强的锁,如表级别的独占锁(X锁),这会导致死锁,因为 COUNT(*)持有 S 锁,而TRUNCATE TABLE需要 X 锁。

3. 事故分析

使用SHOW FULL PROCESSLIST;查询结果如下:

COUNT(*) 和 TRUNCATE TABLE 产生死锁-1

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;

相关文章

Oracle如何使用授予和撤销权限的语法和示例
Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
社区版oceanbase安装
Oracle 导出CSV工具-sqluldr2
ETL数据集成丨快速将MySQL数据迁移至Doris数据库

发布评论