1、概念
哪些情況下,執行一條語句,查詢執行的也是很緩慢呢?
為了便於描述,我還是構造一個表,基於這個表來說明今天的問題。這個表有兩個字段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();
2、第一類:查詢長時間不返回
mysql> select * from t where id=1;
查詢結果長時間不返回。
一般碰到這種情況的話,大概率是表t被鎖住了。接下來分析原因的時候,一般都是首先執行一下show processlist命令,看看當前語句處於什么狀態。
然后我們再針對每種狀態,去分析它們產生的原因、如何復現,以及如何處理。
(1)等MDL鎖
就是使用show processlist命令查看Waiting for table metadata lock的示意圖。
但是,由於在show processlist的結果里面,session A的Command列是“Sleep”,導致查找起來很不方便。不過有了performance_schema和sys系統庫以后,就方便多了。(MySQL啟動時需要設置performance_schema=on,相比於設置為off會有10%左右的性能損失)
通過查詢sys.schema_table_lock_waits這張表,我們就可以直接找出造成阻塞的process id,把這個連接用kill 命令斷開即可。
但是,由於在show processlist的結果里面,session A的Command列是“Sleep”,導致查找起來很不方便。不過有了performance_schema和sys系統庫以后,就方便多了。(MySQL啟動時需要設置performance_schema=on,相比於設置為off會有10%左右的性能損失)
通過查詢sys.schema_table_lock_waits這張表,我們就可以直接找出造成阻塞的process id,把這個連接用kill 命令斷開即可。
(2)等flush
在表t上,執行下面的SQL語句:
mysql> select * from information_schema.processlist where id=1;
查出來這個線程的狀態是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語句。
(3)等行鎖
可以通過sys.innodb_lock_waits 表查到。
查詢方法是:
mysql> select * from t sys.innodb_lock_waits where locked_table=`'test'.'t'`\G
可以看到,這個信息很全,4號線程是造成堵塞的罪魁禍首。而干掉這個罪魁禍首的方式,就是KILL QUERY 4或KILL 4。
不過,這里不應該顯示“KILL QUERY 4”。這個命令表示停止4號線程當前正在執行的語句,而這個方法其實是沒有用的。因為占有行鎖的是update語句,這個語句已經是之前執行完成了的,現在執行KILL QUERY,無法讓這個事務去掉id=1上的行鎖。
實際上,KILL 4才有效,也就是說直接斷開這個連接。這里隱含的一個邏輯就是,連接被斷開的時候,會自動回滾這個連接里面正在執行的線程,也就釋放了id=1上的行鎖。
3、第二類,查詢慢
mysql> select * from t where c=50000 limit 1;
由於字段c上沒有索引,這個語句只能走id主鍵順序掃描,因此需要掃描5萬行。
作為確認,你可以看一下慢查詢日志。注意,這里為了把所有語句記錄到slow log里,我在連接后先執行了 set long_query_time=0,將慢查詢日志的時間閾值設置為0。
Rows_examined顯示掃描了50000行。你可能會說,不是很慢呀,11.5毫秒就返回了,我們線上一般都配置超過1秒才算慢查詢。但你要記住:壞查詢不一定是慢查詢。我們這個例子里面只有10萬行記錄,數據量大起來的話,執行時間就線性漲上去了。
掃描行數多,所以執行慢,這個很好理解。
帶lock in share mode的SQL語句,是當前讀,因此會直接讀到1000001這個結果,所以速度很快;而select * from t where id=1這個語句,是一致性讀,因此需要從1000001開始,依次執行undo log,執行了100萬次以后,才將1這個結果返回。
注意,undo log里記錄的其實是“把2改成1”,“把3改成2”這樣的操作邏輯,畫成減1的目的是方便你看圖。
4、小結
執行“查一行”,可能會出現的被鎖住和執行慢的例子。這其中涉及到了表鎖、行鎖和一致性讀的概念。
在實際使用中,碰到的場景會更復雜。但大同小異,可以按照定位方法,來定位並解決問題。