欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

MySQL 笔记整理(19) --为什么我只查一行的语句,也执行这么慢?

程序员文章站 2023-09-09 18:27:54
笔记记录自林晓斌(丁奇)老师的《MySQL实战45讲》 (本篇内图片均来自丁奇老师的讲解,如有侵权,请联系我删除) 19) --为什么我只查一行的语句,也执行这么慢? 需要说明一下,如果MySQL数据库本身就有很大的压力,导致数据库服务器CPU占用率很高或ioutil(IO利用率)很高,这种情况下所 ......

笔记记录自林晓斌(丁奇)老师的《mysql实战45讲》

(本篇内图片均来自丁奇老师的讲解,如有侵权,请联系我删除)

19) --为什么我只查一行的语句,也执行这么慢?

  需要说明一下,如果mysql数据库本身就有很大的压力,导致数据库服务器cpu占用率很高或ioutil(io利用率)很高,这种情况下所有的语句执行都有可能变慢,不属于我们今天的讨论范围。为了便于描述,我们构造一个表,基于这个表来说明问题。这个表有两个字段id和c,并且我们事先插入了10万行记录。

mysql> create table `t` (
  `id` int(11) not null,
  `c` int(11) default null,
  primary key (`id`)
) engine=innodb;

delimiter ;;
create procedure idata()
begin
  declare i int;
  set i=1;
  while(i<=100000)do
    insert into t values(i,i);
    set i=i+1;
  end while;
end;;
delimiter ;

call idata();

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

  执行如下语句 select * from t where id = 1;结果很长时间不返回结果,这是为什么呢?一般这种情况,大概率是表t被锁住了。这种时候我们就需要show processlist命令来查看当前语句处于什么状态了。然后根据每种状态,分析原因,如何复现以及如何处理。

等mdl锁:

  当我们使用show processlist命令时如果看到了 waiting for table metadata lock的结果,就表示有一个线程正在表t上请求或者持有mdl写锁,把select语句堵住了。我们可以这么来复现:

MySQL 笔记整理(19) --为什么我只查一行的语句,也执行这么慢?

 session a通过lock table命令持有表t的mdl写锁,而session b的查询需要获取mdl读锁。所以,session b进入等待状态。而这类问题的处理方式,就是找到持有mdl写锁的家伙,然后把它kill掉。但是,由于在show processlist的结果里面,session a的command列是“sleep”,导致查找起来不方便。不过有了performance_schema和sys系统库以后,就方便多了。(mysql启动时需要设置performance_schema=on,相比于设置为off,会有10%的性能损失)。通过查询sys.schem_table_lock_waits这张表,我们就可以直接找出造成阻塞的process id,把这个连接用kill命令断开即可。

等flush

  如果我们的show processlist看到了"waiting for table flush",说明现在有一个线程正要对表t做flush操作。mysql里面对表做flush操作的用法,一般有下面两个:

flush tables t with read lock;

flush tables with read lock;

  如果指定了表t的话,代表只关闭表t,如果不指定具体的表名,则表示关闭mysql里的所有打开的表。但正常情况下,这两个语句执行起来都很快,触发它们也被别的线程堵住了。

  所以,出现waiting for table flush状态的可能情况是:有一个flush tables命令被别的语句堵住了,然后它又堵住了我们的select语句。你可以按下面的方式复现:

MySQL 笔记整理(19) --为什么我只查一行的语句,也执行这么慢?

  这样在session a中每行都会调用sleep(1),这样表t一直是被session"打开",然后,session b的flush tables命令要去关闭表t,就需要等待session a的查询结束。这样,session c要再次查询的话,就会被flush命令堵住了。这个例子的处理方式很前面的一样,就不再赘述了。

等行锁:

  经过上面的考验,我们的select语句终于来到了引擎里

mysql> select *from t where id - 1 lock in share model;

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

MySQL 笔记整理(19) --为什么我只查一行的语句,也执行这么慢?

  这时如果你使用show processlist命令会发现statistics。我们来看复现步骤,sessiona启动了事务,占用写锁,还不提交,是导致sessionb被堵住的原因。这个问题不难分析,但问题是怎么查出是谁占用了这个写锁,如果你的mysql5.7或以上版本,你可以通过sys.innodb_lock_waits表查到,查询命令是:

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

  但这里有一个需要注意的地方,当你查出罪魁祸首,比如线程4,你不能直接使用kill query 4;这个命令表示停止4号线程当前正在执行的语句,而这个方法其实是没有用的。因为占有行锁的是update语句,这个语句已经是之前执行完成了的,现在执行kill query,无法让这个事务去掉id=1的行锁。实际上,kill 4 才有效。也就是说直接断开这个连接。这里隐含的一个逻辑是,连接被断开的时候,会自动回滚这个连接里面正在执行的线程,也就释放了id=1的行锁。

第二类:查询慢

  我们先来看一条sql语句

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

  由于字段c上没有索引,这个语句只能走id主键顺序扫描,因此需要扫描50000行。你可以先在慢查询日志里确认一下。为了确保被慢查询日志记录下来,你可以先执行set long_query_time=0。将慢查询日志的时间阈值设置为0.虽然实际上你可能会看到返回结果不是很慢,但有一个原则是这样:坏查询不一定是慢查询。我们这个例子里面只有10万行记录,数据量大起来的话,执行时间就会线性涨上去了。

  扫描行数多,执行慢,这个很好理解。我们再来看一个只扫描了一行,但是执行很慢的语句。执行的语句是这样的。

mysql>select * from t where id=1;

 它的查询日志是这样的:

MySQL 笔记整理(19) --为什么我只查一行的语句,也执行这么慢?

  还是这个查询,如果我们再加上 lock in share model,执行时间居然会变快:

MySQL 笔记整理(19) --为什么我只查一行的语句,也执行这么慢?

  这是为什么呢?按理来说,加上了锁不应该更慢吗?我们再来看一个提示信息,下图是这两个语句的执行输出结果:

MySQL 笔记整理(19) --为什么我只查一行的语句,也执行这么慢?

我们的第一个查询返回的结果是c=1,而带lock in share mode的语句返回的是c=1000001。如果你还是没有头绪,也别着急,我们来看看复现步骤:

MySQL 笔记整理(19) --为什么我只查一行的语句,也执行这么慢?

你看到了,session a先用start transaction with consistent snapshot命令启动了一个事务,之后sessionb才开始执行update语句。sessionb执行完100万次update语句后,id=1这一行处于什么状态呢?你可以从下图找到答案:

MySQL 笔记整理(19) --为什么我只查一行的语句,也执行这么慢?

  session b 更新完成100万次,生成了100万个回滚日志(undo log).带lock in share mode的sql语句,是当前读,因此会直接读到10000001这个结果,所以速度很快;而select * from t where id = 1这个语句,是一致性读,因此需要从1000001开始,依次执行updo log,执行了100万次一行,才将结果1返回。

  注意,undo log里面的记录其实是“把2改成1”这样,而不是图中的“-1”;