1. mysql> CREATE TABLE `t` (
  2. `id` int(11) NOT NULL,
  3. `c` int(11) DEFAULT NULL,
  4. PRIMARY KEY (`id`)
  5. ) ENGINE=InnoDB;
  6. delimiter ;;
  7. create procedure idata()
  8. begin
  9. declare i int;
  10. set i=1;
  11. while(i<=100000) do
  12. insert into t values(i,i);
  13. set i=i+1;
  14. end while;
  15. end;;
  16. delimiter ;
  17. call idata();

第一类:查询长时间不返回

mysql> select * from t where id=1;

image.png

图 1 查询长时间不返回

一般碰到这种情况的话,大概率是表 t 被锁住了。接下来分析原因的时候,一般都是首先执行一下 show processlist 命令,看看当前语句处于什么状态。

等 MDL 锁

image.png

图 2 Waiting for table metadata lock 状态示意图

在 MySQL 5.7 版本下复现这个场景,也很容易。如图 3 所示,我给出了简单的复现步骤。

image.png

图 3 MySQL 5.7 中 Waiting for table metadata lock 的复现步骤

session A 通过 lock table 命令持有表 t 的 MDL 写锁,而 session B 的查询需要获取 MDL 读锁。所以,session B 进入等待状态。

由于在 show processlist 的结果里面,session A 的 Command 列是“Sleep”,导致查找起来很不方便。不过有了 performance_schema 和 sys 系统库以后,就方便多了。(MySQL 启动时需要设置 performance_schema=on,相比于设置为 off 会有 10% 左右的性能损失)

通过查询 sys.schema_table_lock_waits 这张表,我们就可以直接找出造成阻塞的 process id,把这个连接用 kill 命令断开即可。

image.png

图 4 查获加表锁的线程 id

等 flush

mysql> select * from information_schema.processlist where id=1;

image.png

图 5 Waiting for table flush 状态示意图

这个状态表示的是,现在有一个线程正要对表 t 做 flush 操作。MySQL 里面对表做 flush 操作的用法,一般有以下两个:

flush tables t with read lock;

flush tables with read lock;

这两个 flush 语句,如果指定表 t 的话,代表的是只关闭表 t;如果没有指定具体的表名,则表示关闭 MySQL 里所有打开的表。

正常这两个语句执行起来都很快,除非它们也被别的线程堵住了。所以,出现 Waiting for table flush 状态的可能情况是:有一个 flush tables 命令被别的语句堵住了,然后它又堵住了我们的 select 语句。

复现步骤如图 6 所示:

image.png

图 6 Waiting for table flush 的复现步骤

image.png

图 7 Waiting for table flush 的 show processlist 结果

等行锁

mysql> select * from t where id=1 lock in share mode;

由于访问 id=1 这个记录时要加读锁,如果这时候已经有一个事务在这行记录上持有一个写锁,我们的 select 语句就会被堵住。

image.png

图 8 行锁复现

image.png

图 9 行锁 show processlist 现场

显然,session A 启动了事务,占有写锁,还不提交,是导致 session B 被堵住的原因。

这个问题并不难分析,但问题是怎么查出是谁占着这个写锁。如果你用的是 MySQL 5.7 版本,可以通过 sys.innodb_lock_waits 表查到。

mysql> select * from t sys.innodb_lock_waits where locked_table='`test`.`t`'\G

image.png

图 10 通过 sys.innodb_lock_waits 查行锁

实际上,KILL 4 才有效,也就是说直接断开这个连接。这里隐含的一个逻辑就是,连接被断开的时候,会自动回滚这个连接里面正在执行的线程,也就释放了 id=1 上的行锁。

第二类:查询慢

mysql> select * from t where c=50000 limit 1;

由于字段 c 上没有索引,这个语句只能走 id 主键顺序扫描,因此需要扫描 5 万行。

image.png

图 11 全表扫描 5 万行的 slow log

我们再看一个只扫描一行,但是执行很慢的语句。

mysql> select * from t where id=1;

image.png

图 12 扫描一行却执行得很慢

image.png

图 14 两个语句的输出结果

image.png

图 15 复现步骤

session B 执行完 100 万次 update 语句后,id=1 这一行处于什么状态呢?你可以从图 16 中找到答案。

image.png

带 lock in share mode 的 SQL 语句,是当前读,因此会直接读到 1000001 这个结果,所以速度很快;而 select * from t where id=1 这个语句,是一致性读,因此需要从 1000001 开始,依次执行 undo log,执行了 100 万次以后,才将 1 这个结果返回。

注意,undo log 里记录的其实是“把 2 改成 1”,“把 3 改成 2”这样的操作逻辑,画成减 1 的目的是方便你看图。