MySQL實戰45講學習筆記:第十九講

1、引子

通常狀況下,若是我跟你說查詢性能優化,你首先會想到一些複雜的語句,想到查詢須要返回大量的數據。但有些狀況下,「查一行」,也會執行得特別慢。今天,我就跟你聊聊
這個有趣的話題,看看什麼狀況下,會出現這個現象。mysql

須要說明的是,若是 MySQL 數據庫自己就有很大的壓力,致使數據庫服務器 CPU 佔用率很高或 ioutil(IO 利用率)很高,這種狀況下全部語句的執行都有可能變慢,不屬於我
們今天的討論範圍。sql

爲了便於描述,我仍是構造一個表,基於這個表來講明今天的問題。這個表有兩個字段 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、第一類:查詢長時間不返回

如圖 1 所示,在表 t 執行下面的 SQL 語句:性能優化

mysql> select * from t where id=1;

查詢結果長時間不返回bash

             

圖 1 查詢長時間不返回服務器

通常碰到這種狀況的話,大機率是表 t 被鎖住了。接下來分析緣由的時候,通常都是首先執行一下 show processlist 命令,看看當前語句處於什麼狀態。網絡

而後咱們再針對每種狀態,去分析它們產生的緣由、如何復現,以及如何處理。session

一、等 MDL 鎖

如圖 2 所示,就是使用 show processlist 命令查看 Waiting for table metadata lock 的示意圖。函數

圖 2 Waiting for table metadata lock 狀態示意圖

實際測試截圖

+----+-----------------+----------------------+------+---------+------+---------------------------------+----------------------------+
| Id | User            | Host                 | db   | Command | Time | State                           | Info                       |
+----+-----------------+----------------------+------+---------+------+---------------------------------+----------------------------+
|  4 | event_scheduler | localhost            | NULL | Daemon  |  184 | Waiting on empty queue          | NULL                       |
|  8 | root            | 192.168.118.85:59418 | test | Query   |  101 | Waiting for table metadata lock | select * from t where id=1 |
|  9 | root            | 192.168.118.83:65443 | test | Query   |    0 | starting                        | show processlist  

出現這個狀態表示的是,如今有一個線程正在表 t 上請求或者持有 MDL 寫鎖,把 select語句堵住了。

實際測試代碼

 

 

 

圖 3 MySQL 5.7 中 Waiting for table metadata lock 的復現步驟


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.schema_table_lock_waits 這張表,咱們就能夠直接找出形成阻塞的process id,把這個鏈接用 kill 命令斷開便可。

實際測試代碼:

mysql> kill 9 ;
1317 - Query execution was interrupted

再次查看:

mysql> select * from t where id=1;
+----+------+
| id | c    |
+----+------+
|  1 |    1 |
+----+------+
1 row in set (0.00 sec)

 圖 4 查獲加表鎖的線程 id

二、等 flush

接下來,我給你舉另一種查詢被堵住的狀況。

我在表 t 上,執行下面的 SQL 語句:

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

這裏,我先賣個關子。

你能夠看一下圖 5。我查出來這個線程的狀態是 Waiting for table flush,你能夠設想一下這是什麼緣由。

圖 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 所示:

圖 6 Waiting for table flush 的復現步驟

 

實際測試截圖:

session A 

session B

session C 

在 session A 中,我故意每行都調用一次 sleep(1),這樣這個語句默認要執行 10 萬秒,在這期間表 t 一直是被 session A「打開」着。而後,session B 的 flush tables t 命令再
要去關閉表 t,就須要等 session A 的查詢結束。這樣,session C 要再次查詢的話,就會被 flush 命令堵住了。

圖 7 是這個復現步驟的 show processlist 結果。這個例子的排查也很簡單,你看到這個show processlist 的結果,確定就知道應該怎麼作了。

圖 7 Waiting for table flush 的 show processlist 結果

實際測試代碼:

mysql> show processlist;
+----+------+-----------------------+------+---------+------+-------------------------+----------------------------+
| Id | User | Host                  | db   | Command | Time | State                   | Info                       |
+----+------+-----------------------+------+---------+------+-------------------------+----------------------------+
|  4 | root | 192.168.118.85:58126  | test | Query   |  219 | User sleep              | select sleep(1) from t     |
|  5 | root | 192.168.118.109:40554 | test | Query   |  160 | Waiting for table flush | flush tables t             |
|  9 | root | 192.168.118.83:54220  | NULL | Sleep   |  619 |                         | NULL                       |
| 10 | root | 192.168.118.83:54221  | test | Sleep   |  402 |                         | NULL                       |
| 11 | root | 192.168.118.83:54241  | test | Sleep   |  393 |                         | NULL                       |
| 12 | root | 192.168.118.83:54310  | test | Query   |   37 | Waiting for table flush | select * from t where id=1 |
| 13 | root | 192.168.118.83:54321  | test | Query   |    0 | starting                | show processlist           |
+----+------+-----------------------+------+---------+------+-------------------------+----------------------------+
7 rows in set

三、等行鎖

如今,通過了表級鎖的考驗,咱們的 select 語句終於來到引擎裏了。

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

上面這條語句的用法你也很熟悉了,咱們在第 8 篇《事務究竟是隔離的仍是不隔離的?》文章介紹當前讀時提到過。

因爲訪問 id=1 這個記錄時要加讀鎖,若是這時候已經有一個事務在這行記錄上持有一個寫鎖,咱們的 select 語句就會被堵住。

復現步驟和現場以下:

圖 8 行鎖復現

圖 9 行鎖 show processlist 現場

復現截圖:

session A

session B

顯然,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

圖 10 經過 sys.innodb_lock_waits 查行鎖

測試命令及截圖

命令行報錯

mysql> select * from t sys.innodb_lock_waits where locked_table=`'test'.'t'`\G;
1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '.innodb_lock_waits where locked_table=`'test'.'t'`\G' at line 1
mysql> 

剛開始我安裝的mysql8.0,說版本不對,因而我更換成5.7.19命令測試故障依舊

我經過Navicat Premium軟件鏈接截圖入下

 

能夠看到,這個信息很全,4 號線程是形成堵塞的罪魁禍首。而幹掉這個罪魁禍首的方式,就是 KILL QUERY 4 或 KILL 4

不過,這裏不該該顯示「KILL QUERY 4」。這個命令表示中止 4 號線程當前正在執行的語句,而這個方法實際上是沒有用的。由於佔有行鎖的是 update 語句,這個語句已是之
前執行完成了的,如今執行 KILL QUERY,沒法讓這個事務去掉 id=1 上的行鎖。

實際上,KILL 4 纔有效,也就是說直接斷開這個鏈接。這裏隱含的一個邏輯就是,鏈接被斷開的時候,會自動回滾這個鏈接裏面正在執行的線程,也就釋放了 id=1 上的行鎖。

3、第二類:查詢慢

通過了重重封「鎖」,咱們再來看看一些查詢慢的例子。

先來看一條你必定知道緣由的 SQL 語句:

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

因爲字段 c 上沒有索引,這個語句只能走 id 主鍵順序掃描,所以須要掃描 5 萬行。做爲確認,你能夠看一下慢查詢日誌。注意,這裏爲了把全部語句記錄到 slow log 裏,
我在鏈接後先執行了 set long_query_time=0,將慢查詢日誌的時間閾值設置爲 0。

因爲字段 c 上沒有索引,這個語句只能走 id 主鍵順序掃描,所以須要掃描 5 萬行。做爲確認,你能夠看一下慢查詢日誌。注意,這裏爲了把全部語句記錄到 slow log 裏,
我在鏈接後先執行了 set long_query_time=0,將慢查詢日誌的時間閾值設置爲 0。

圖 11 全表掃描 5 萬行的 slow log

實際測試截圖:

Rows_examined 顯示掃描了 50000 行。你可能會說,不是很慢呀,11.5 毫秒就返回了,咱們線上通常都配置超過 1 秒纔算慢查詢。但你要記住:壞查詢不必定是慢查詢。我
們這個例子裏面只有 10 萬行記錄,數據量大起來的話,執行時間就線性漲上去了。掃描行數多,因此執行慢,這個很好理解。

可是接下來,咱們再看一個只掃描一行,可是執行很慢的語句。如圖 12 所示,是這個例子的 slow log。能夠看到,執行的語句是

mysql> select * from t where id=1;

雖然掃描行數是 1,但執行時間卻長達 800 毫秒。

圖 12 掃描一行卻執行得很慢

實際測試截圖:

是否是有點奇怪呢,這些時間都花在哪裏了?

若是我把這個 slow log 的截圖再往下拉一點,你能夠看到下一個語句,select * from twhere id=1 lock in share mode,執行時掃描行數也是 1 行,執行時間是 0.2 毫秒。

圖 13 加上 lock in share mode 的 slow log

實際測試截圖:

看上去是否是更奇怪了?按理說 lock in share mode 還要加鎖,時間應該更長才對啊。可能有的同窗已經有答案了。若是你尚未答案的話,我再給你一個提示信息,圖 14 是
這兩個語句的執行輸出結果。

圖 14 兩個語句的輸出結果

第一個語句的查詢結果裏 c=1,帶 lock in share mode 的語句返回的是 c=1000001。看到這裏應該有更多的同窗知道緣由了。若是你仍是沒有頭緒的話,也彆着急。我先跟你說
明一下復現步驟,再分析緣由。

圖 15 復現步驟

 

session A 實際測試代碼以下

mysql> start transaction with consistent snapshot;
Query OK, 0 rows affected (0.00 sec)

mysql> use test;
Database changed
mysql> select * from t where id=1;
+----+------+
| id | c    |
+----+------+
|  1 |    1 |
+----+------+
1 row in set (0.00 sec)

mysql> select * from t where id=1 lock in share mode;
+----+------+
| id | c    |
+----+------+
|  1 |    2 |
+----+------+
1 row in set (0.00 sec)

你看到了,session A 先用 start transaction with consistent snapshot 命令啓動了一個事務,以後 session B 纔開始執行 update 語句。

session A 實際測試代碼以下

mysql> use test
Database changed
mysql> update t set c=c+1 where id=1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

session B 執行完 100 萬次 update 語句後,id=1 這一行處於什麼狀態呢?你能夠從圖16 中找到答案。

 圖 16 id=1 的數據狀態

session B 更新完 100 萬次,生成了 100 萬個回滾日誌 (undo log)。

帶 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、小結

今天我給你舉了在一個簡單的表上,執行「查一行」,可能會出現的被鎖住和執行慢的例子。這其中涉及到了表鎖、行鎖和一致性讀的概念。

在實際使用中,碰到的場景會更復雜。但大同小異,你能夠按照我在文章中介紹的定位方法,來定位並解決問題。

最後,我給你留一個問題吧。

咱們在舉例加鎖讀的時候,用的是這個語句,select * from t where id=1 lock in sharemode。因爲 id 上有索引,因此能夠直接定位到 id=1 這一行,所以讀鎖也是隻加在了這一行上。

但若是是下面的 SQL 語句,

begin;
select * from t where c=5 for update;
commit;

這個語句序列是怎麼加鎖的呢?加的鎖又是何時釋放呢?

你能夠把你的觀點和驗證方法寫在留言區裏,我會在下一篇文章的末尾給出個人參考答案。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。

5、上期問題時間

在上一篇文章最後,我留給你的問題是,但願你能夠分享一下以前碰到過的、與文章中相似的場景。

@封建的風 提到一個有趣的場景,值得一說。我把他的問題重寫一下,表結構以下:

mysql> CREATE TABLE `table_a` (
  `id` int(11) NOT NULL,
  `b` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `b` (`b`)
) ENGINE=InnoDB;

假設如今表裏面,有 100 萬行數據,其中有 10 萬行數據的 b 的值是’1234567890’,
假設如今執行語句是這麼寫的:

mysql> select * from table_a where b='1234567890abcd';

這時候,MySQL 會怎麼執行呢?

最理想的狀況是,MySQL 看到字段 b 定義的是 varchar(10),那確定返回空呀。惋惜,MySQL 並無這麼作

那要不,就是把’1234567890abcd’拿到索引裏面去作匹配,確定也沒可以快速判斷出索引樹 b 上並無這個值,也很快就能返回空結果。但實際上,MySQL 也不是這麼作的。

這條 SQL 語句的執行很慢,流程是這樣的:

1. 在傳給引擎執行的時候,作了字符截斷。由於引擎裏面這個行只定義了長度是 10,因此只截了前 10 個字節,就是’1234567890’進去作匹配;

2. 這樣知足條件的數據有 10 萬行;

3. 由於是 select *, 因此要作 10 萬次回表;

4. 可是每次回表之後查出整行,到 server 層一判斷,b 的值都不是’1234567890abcd’;

5. 返回結果是空。

這個例子,是咱們文章內容的一個很好的補充。雖然執行過程當中可能通過函數操做,可是最終在拿到結果後,server 層仍是要作一輪判斷的。

6、經典留言

一、某、人

最近幾張乾貨愈來愈多了,很實用,收穫很多.先回答今天的問題
版本5.7.13

rc模式下:
session 1:
begin;
select * from t where c=5 for update;
session 2:
delete from t where c=10 --等待
session 3:
insert into t values(100001,8) --成功
session 1:
commit
session 2:事務執行成功
rr模式下:
begin;
select * from t where c=5 for update;
session 2:
delete from t where c=10 --等待
session 3:
insert into t values(100001,8) --等待
session 1:
commit
session 2:事務執行成功
session 3:事務執行成功

從上面這兩個簡單的例子,能夠大概看出上鎖的流程.
無論是rr模式仍是rc模式,這條語句都會先在server層對錶加上MDL S鎖,而後進入到引擎層。

rc模式下,因爲數據量不大隻有10W。經過實驗能夠證實session 1上來就把該表的全部行都鎖住了。
致使其餘事務要對該表的全部現有記錄作更新,是阻塞狀態。爲何insert又能成功?
說明rc模式下for update語句沒有上gap鎖,因此不阻塞insert對範圍加插入意向鎖,因此更新成功。
session 1commit後,session 2執行成功。代表全部行的x鎖是在事務提交完成之後才釋放。

rr模式下,session 1和session 2與rc模式下都同樣,說明rr模式下也對全部行上了X鎖。
惟一的區別是insert也等待了,是由於rr模式下對沒有索引的更新,聚簇索引上的全部記錄,都被加上了X鎖。其次,聚簇索引每條記錄間的間隙(GAP),也同時被加上了GAP鎖。因爲gap鎖阻塞了insert要加的插入意向鎖,致使insert也處於等待狀態。只有當session 1 commit完成之後。session 1上的全部鎖纔會釋放,S2,S3執行成功

因爲例子中的數據量還比較小,若是數據量達到千萬級別,就比較直觀的能看出,上鎖是逐行上鎖的一個過程.掃描一條上一條,直到全部行掃描完,rc模式下對全部行上x鎖。rr模式下不只對全部行上X鎖,還對全部區間上gap鎖.直到事務提交或者回滾完成後,上的鎖纔會被釋放。

 做者回復

做者回復: 分析得很是好。
兩個模式下,各增長一個session 4 : update t set c=100 where id=10看看哦

基本就全了👍🏿

二、某、人

老師我請教一個問題:
flush tables中close table的意思是說的把open_tables裏的表所有關閉掉?下次若是有關於某張表的操做
又把frm file緩存進Open_table_definitions,把表名緩存到open_tables,仍是open_table只是一個計數?
不是特別明白flush table和打開表是個什麼流程

做者回復:

Flush tables是會關掉表,而後下次請求從新讀表信息的

第一次打開表其實就是open_table_definitions,包括讀表信息一類的

以後再有查詢就是拷貝一個對象,加一個計數這樣的

三、godtrue

課前思考1:爲啥只查一行的語句,也執行這麼慢?查的慢,基本上就是索引使用的問題,和查一行仍是N行(N不是巨大),沒有必然聯繫。查一行慢,猜想沒有走索引查詢,且數據量比較大。課後思考1:閱後發現本身的無知,只查詢一行的語句,也比較慢,緣由從大到小可分爲三種狀況?第一MySQL數據庫自己被堵住了,好比:系統或網絡資源不夠第二SQL語句被堵住了,好比:表鎖,行鎖等,致使存儲引擎不執行對應的SQL語句第三確實是索引使用不當,沒有走索引第四是表中數據的特色致使的,走了索引,但回表次數龐大感謝老師的分享,真是醍醐灌頂呀😄

相關文章
相關標籤/搜索