先构造一个表,在这个表中有两个字段id和c,在这个表中插入了10w条数据。
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();
在表t上执行下面的sql语句。
select * from t where id=1;
执行这个语句长时间不会返回结果。
那么什么时候会出现这种现象呢?这种语句会出现长时间不返回结果的问题大概率是因为表t被锁住了,分析原因的第一步总是执行show processlist命令,看一下我们关注的sql语句此时正处在什么状态。show processlist命令的执行结果显示的是什么?执行结果显示的是此时在Mysql中的每个线程现在正在做什么。
在执行show processlist命令之后,我们可以看到下图。
在showprocesslist命令的执行结果中,我们可以看到有一个线程的状态是正在等待表的元数据锁,如果在Mysql中有某个线程正在等待表的元数据写锁,又或者是它已经持有了表的元数据写锁,都会导致把别的线程的select语句给堵上。为什么是执行select语句的线程正在等待表的元数据写锁呢?
那么怎么复现show processlist的执行结果呢?按照下表去做就行了。
会话A中执行了lock table … write语句,所以会话A会在表t上加上元数据写锁,在会话B中执行select语句之前要先在表t上加上元数据读锁,元数据读锁和元数据写锁是互斥的,所以select语句就没办法执行,解决这个问题的办法是,找到正在持有表的元数据写锁的线程,然后把这个线程kill掉就可以了。
但是从show processlist命令的执行结果我们可以看到,id=7的数据行中的command选项的值是sleep,那么怎么找到这个数据行对应的线程呢?如果command选项的值不是sleep,那么还有别的方法可以找到这个数据行对应的线程吗?
我们可以从sys.schema_table_lock_waits这张表中查到,使用下面的sql语句就可以查出造成阻塞的线程的id,然后使用kill命令杀死这个线程就可以了。造成阻塞的线程指的是在表上加元数据写锁的线程吗?为什么在查询结果中没有看到具体的线程id呢?
还有另一种堵住select语句的情况。
先执行下面这个sql语句,infomation_schema是默认创建的数据库,processlist是这个数据库中的一张表。
select * from information_schema.processlist where id=1;
下图是这个sql语句的执行结果。
我们可以看到这个线程的执行结果中的state字段的值是waiting for table flush,这个值的含义是现在有一个线程正在等待对表t做flush操作,在Mysql中有两种常见的flush语句,
flush tables t with read lock;
flush tables with read lock;
如果在语句中指定了表名,表示的是只把表t给关闭了,如果没有指定具体的表名,表示的是把这个库中所有打开的表都给关闭了,这个语句的作用不是给数据库加全局锁的吗?加全局锁的目的是对数据库中的数据进行备份,防止在备份的过程中数据被修改。如果加全局锁会导致把表给关闭了,那么还能从表中读取数据吗?
但是这两个语句执行起来都很快,除非这两个语句也被别的线程堵住了。意思是表锁把select语句堵住了,别的线程又把表锁堵住了,所以导致select语句一直没办法执行。
那么怎么复现呢?如下所示。
在会话A中,每从表t中取出一个数据行就要睡眠1s,而表t中一共有10w个数据行,所以这个会话总共要睡眠10w秒,在这期间表t一直在被会话A占用,在之后的某个时刻,会话B执行flush tables t;语句,想要把表t关上,但是此时表t正在被会话A使用,所以没办法关闭,要等到会话A中的查询语句结束之后才能把表t关闭了,而会话C中的查询则会被会话B中将要在会话A中查询语句执行完之后执行的flush命令阻塞了。会话C中的查询是被执行flush命令时在表t上加的表锁阻塞的吗?我觉得应该不是,因为表锁不会阻塞查询语句吧。那么为什么flush命令会阻塞select语句呢?是因为flush命令把表给关上了,所以就没办法执行select语句了吗?
select sleep(1) from t语句的具体执行过程是什么样的?怎么结束一个会话?如果在select sleep(1) from t语句执行完之后一直不结束会话,会话B中的flush语句会被执行吗?
这时再执行一次show processlist命令,执行结果如下。
看到waiting for table flush,就知道这次select语句的阻塞是因为flush操作,怎么解决select语句被阻塞的问题呢?把id=4的数据行对应的线程kill掉就行了。
假设现在我们执行的sql语句是
select * from t where id=1 lock in share mode;
在执行这条sql语句的时候会给id=1的数据行加上读锁,所以如果已经有一个事务给这个数据行加上了写锁,那么这条sql语句就会被堵住。
按照下面的步骤就可以模拟这种场景。
然后执行show processlist命令。
我们可以看到在会话A中启动了一个事务,然后在这个事务中执行了一条发生在id=1的数据行上的更新语句,所以这个事务就在id=1的数据行上加了写锁,由于事务一直没有提交,所以写锁一直没有被释放,会话B中的select语句想要执行需要先在id=1的数据行上加读锁,而读写锁是互斥的,所以会话B中的select语句没办法执行。
从show processlist的执行结果我们可以看到应该是id=10的数据行对应的线程持有了写锁,从而阻塞了id=8的数据行对应的线程在表t中id=1的数据行上加读锁,并且这个线程在执行show processlist命令的时候什么都没做,所以command选项的值是sleep。
所以我们想要解决这个阻塞问题,就要找出来是谁占用了这个写锁,然后再把这个线程kill掉。
我们可以在sys.innodb_lock_wait表中查到是哪个线程占用了这个写锁,具体的sql语句如下。
select * from sys.innodb_lock_waits where locked_table='`test`.`t`'\G
这个sql语句的执行结果如下。
sql语句的执行结果中的blocking_pid选项表明造成阻塞的线程是4号线程,那么我们怎么解决这个阻塞问题呢?
有两种办法,kill query 4和kill 4。但是在这个阻塞场景中,kill query 4是不起作用的,因为kill query 4的作用是停止4号线程当前正在执行的语句,而对于这个模拟场景来说,update语句已经执行完了,当前并没有正在执行的语句,所以执行kill query 4命令是没有效果的,没有效果指的是执行之前是什么样子执行之后还是什么样子,在执行kill query 4命令后事务仍然没有提交,事务加在id=1的数据行上的写锁自然也没有被释放。
在这个模拟场景中,kill 4命令才起作用,kill 4命令的作用是直接断开会话所在的连接,当连接被断开的时候,会自动回滚这个连接
下面说一些查询慢的例子。
select * from t where c=50000 limit 1;
因为在字段c上没有建立索引,所以在执行这个语句时是对主键索引树做全表扫描,总共需要扫描50000行。
我们可以看慢查询日志验证一下说的对不对,为了能把所有的查询语句都记录到慢查询日志中,我们要在建立连接成功后先执行语句set long_query_time=0;将sql语句放入慢查询日志的阈值设置为0,在慢查询日志中我们能看到如下信息。
rows_examined字段的值就是这个sql语句执行过程中扫描的行数,也就是扫描了50000行,我们可以看到查询时间是0.011543秒,也就是11.53毫秒,也不算慢啊,在线上一般都配置成超过1s才算慢查询,但是我们要明确一件事,坏查询并不一定是慢查询,当这张表中的数据量大起来了,这个sql语句的执行时间就近似线性增长上去了。
由于扫描的行数很多,所以执行起来很慢,这个因果关系符合我们的认知,那么为什么下面这个只会扫描一行数据,但是执行起来也很慢呢?
select * from t where id=1;
我们来看一下它的慢查询日志。
虽然实际的扫描行数只有1,但是执行时间却是800ms,那么是哪个地方比较耗时的呢?
在慢查询日志中紧跟在上图后面的是select from t where id=1 lock in share mode;语句的执行情况,这条语句的扫描行数也是1行,但是执行时间只要0.2毫秒。
这个现象看起来就更奇怪了,如果在某个时刻只有一个会话连接上Mysql,然后在这个会话中执行select from t where id=1;和select * from t where id=1 lock in share mode;,那一定是lock in share mode语句的执行时间更长,因为在执行lock in share mode语句时相比前者还多了一个在数据行上加锁的过程。
所以出现这种现象一定不是一个会话能构造出来的。
我们先来看一下这两个sql语句的执行结果。
第一个语句的查询结果是c=1,带lock in share mode的语句的查询结果是c=1000001,为什么会出现这种现象呢?
我们可以按照下面的流程来复现这个场景。
在会话A中使用start transaction with consistent snapshot;语句启动了一个事务之后,才开始在会话B中执行更新语句。