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

1、本節概述

在上一篇文章中,我提到 MySQL 對自增主鍵鎖作了優化,儘可能在申請到自增 id 之後,就釋放自增鎖。bash

所以,insert 語句是一個很輕量的操做。不過,這個結論對於「普通的 insert 語句」纔有效。也就是說,還有些 insert 語句是屬於「特殊狀況」的,在執行過程當中須要給其餘資源
加鎖,或者沒法在申請到自增 id 之後就立馬釋放自增鎖。session

那麼,今天這篇文章,咱們就一塊兒來聊聊這個話題。併發

2、insert … select 語句

咱們先從昨天的問題提及吧。表 t 和 t2 的表結構、初始化數據語句以下,今天的例子咱們仍是針對這兩個表展開。優化

CREATE TABLE `t` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `c` (`c`)
) ENGINE=InnoDB;

insert into t values(null, 1,1);
insert into t values(null, 2,2);
insert into t values(null, 3,3);
insert into t values(null, 4,4);

create table t2 like t

如今,咱們一塊兒來看看爲何在可重複讀隔離級別下,binlog_format=statement 時執行:spa

insert into t2(c,d) select c,d from t;

這個語句時,須要對錶 t 的全部行和間隙加鎖呢?線程

其實,這個問題咱們須要考慮的仍是日誌和數據的一致性。咱們看下這個執行序列:3d

圖 1 併發 insert 場景日誌

實際的執行效果是,若是 session B 先執行,因爲這個語句對錶 t 主鍵索引加了 (-∞,1] 這個 next-key lock,會在語句執行完成後,才容許 session A 的 insert 語句執行。orm

但若是沒有鎖的話,就可能出現 session B 的 insert 語句先執行,可是後寫入 binlog 的狀況。因而,在 binlog_format=statement 的狀況下,binlog 裏面就記錄了這樣的語句序列:對象

insert into t values(-1,-1,-1);
insert into t2(c,d) select c,d from t;

這個語句到了備庫執行,就會把 id=-1 這一行也寫到表 t2 中,出現主備不一致。

3、insert 循環寫入

固然了,執行 insert … select 的時候,對目標表也不是鎖全表,而是隻鎖住須要訪問的資源。

一、慢查詢日誌 -- 將數據插入表 t2

若是如今有這麼一個需求:要往表 t2 中插入一行數據,這一行的 c 值是表 t 中 c 值的最大值加 1。

此時,咱們能夠這麼寫這條 SQL 語句 :

insert into t2(c,d)  (select c+1, d from t force index(c) order by c desc limit 1);

這個語句的加鎖範圍,就是表 t 索引 c 上的 (3,4] 和 (4,supremum] 這兩個 next-keylock,以及主鍵索引上 id=4 這一行。

它的執行流程也比較簡單,從表 t 中按照索引 c 倒序,掃描第一行,拿到結果寫入到表 t2中。

所以整條語句的掃描行數是 1。

這個語句執行的慢查詢日誌(slow log),以下圖所示:

圖 2 慢查詢日誌 -- 將數據插入表 t2

經過這個慢查詢日誌,咱們看到 Rows_examined=1,正好驗證了執行這條語句的掃描行數爲 1。

二、慢查詢日誌 -- 將數據插入表 t

那麼,若是咱們是要把這樣的一行數據插入到表 t 中的話:

insert into t(c,d)  (select c+1, d from t force index(c) order by c desc limit 1);

語句的執行流程是怎樣的?掃描行數又是多少呢?

這時候,咱們再看慢查詢日誌就會發現不對了。

圖 3 慢查詢日誌 -- 將數據插入表 t

能夠看到,這時候的 Rows_examined 的值是 5。

三、 explain 結果

我在前面的文章中提到過,但願你都可以學會用 explain 的結果來「腦補」整條語句的執行過程。今天,咱們就來一塊兒試試

如圖 4 所示就是這條語句的 explain 結果。

圖 4 explain 結果

從 Extra 字段能夠看到「Using temporary」字樣,表示這個語句用到了臨時表。也就是說,執行過程當中,須要把表 t 的內容讀出來,寫入臨時表。

圖中 rows 顯示的是 1,咱們不妨先對這個語句的執行流程作一個猜想:若是說是把子查詢的結果讀出來(掃描 1 行),寫入臨時表,而後再從臨時表讀出來(掃描 1 行),寫回
表 t 中。那麼,這個語句的掃描行數就應該是 2,而不是 5。

因此,這個猜想不對。實際上,Explain 結果裏的 rows=1 是由於受到了 limit 1 的影響。

四、查看 Innodb_rows_read 變化

從另外一個角度考慮的話,咱們能夠看看 InnoDB 掃描了多少行。如圖 5 所示,是在執行這個語句先後查看 Innodb_rows_read 的結果。

圖 5 查看 Innodb_rows_read 變化

能夠看到,這個語句執行先後,Innodb_rows_read 的值增長了 4。由於默認臨時表是使用 Memory 引擎的,因此這 4 行查的都是表 t,也就是說對錶 t 作了全表掃描。

這樣,咱們就把整個執行過程理清楚了:

1. 建立臨時表,表裏有兩個字段 c 和 d。
2. 按照索引 c 掃描表 t,依次取 c=四、三、二、1,而後回表,讀到 c 和 d 的值寫入臨時表。這時,Rows_examined=4。
3. 因爲語義裏面有 limit 1,因此只取了臨時表的第一行,再插入到表 t 中。這時,Rows_examined 的值加 1,變成了 5。

也就是說,這個語句會致使在表 t 上作全表掃描,而且會給索引 c 上的全部間隙都加上共享的 next-key lock。因此,這個語句執行期間,其餘事務不能在這個表上插入數據。

至於這個語句的執行爲何須要臨時表,緣由是這類一邊遍歷數據,一邊更新數據的狀況,若是讀出來的數據直接寫回原表,就可能在遍歷過程當中,讀到剛剛插入的記錄,新插
入的記錄若是參與計算邏輯,就跟語義不符。

因爲實現上這個語句沒有在子查詢中就直接使用 limit 1,從而致使了這個語句的執行須要遍歷整個表 t。它的優化方法也比較簡單,就是用前面介紹的方法,先 insert into 到臨時
表 temp_t,這樣就只須要掃描一行;而後再從表 temp_t 裏面取出這行數據插入表 t1。

固然,因爲這個語句涉及的數據量很小,你能夠考慮使用內存臨時表來作這個優化。使用內存臨時表優化時,語句序列的寫法以下:

create temporary table temp_t(c int,d int) engine=memory;
insert into temp_t  (select c+1, d from t force index(c) order by c desc limit 1);
insert into t select * from temp_t;
drop table temp_t;

4、insert 惟一鍵衝突

一、惟一鍵衝突加鎖

前面的兩個例子是使用 insert … select 的狀況,接下來我要介紹的這個例子就是最多見的insert 語句出現惟一鍵衝突的狀況。

對於有惟一鍵的表,插入數據時出現惟一鍵衝突也是常見的狀況了。我先給你舉一個簡單的惟一鍵衝突的例子。

圖 6 惟一鍵衝突加鎖

這個例子也是在可重複讀(repeatable read)隔離級別下執行的。能夠看到,session B要執行的 insert 語句進入了鎖等待狀態。

也就是說,session A 執行的 insert 語句,發生惟一鍵衝突的時候,並不僅是簡單地報錯返回,還在衝突的索引上加了鎖。咱們前面說過,一個 next-key lock 就是由它右邊界的
值定義的。這時候,session A 持有索引 c 上的 (5,10] 共享 next-key lock(讀鎖)。

至於爲何要加這個讀鎖,其實我也沒有找到合理的解釋。從做用上來看,這樣作能夠避免這一行被別的事務刪掉。

這裏官方文檔有一個描述錯誤,認爲若是衝突的是主鍵索引,就加記錄鎖,惟一索引才加next-key lock。但實際上,這兩類索引衝突加的都是 next-key lock。

備註:這個 bug,是我在寫這篇文章查閱文檔時發現的,已經發給官方並被verified 了。

二、 惟一鍵衝突 -- 死鎖

有同窗在前面文章的評論區問到,在有多個惟一索引的表中併發插入數據時,會出現死鎖。可是,因爲他沒有提供復現方法或者現場,我也沒法作分析。因此,我建議你在評論
區發問題的時候,儘可能同時附上覆現方法,或者現場信息,這樣我纔好和你一塊兒分析問題。

這裏,我就先和你分享一個經典的死鎖場景,若是你還遇到過其餘惟一鍵衝突致使的死鎖場景,也歡迎給我留言。

在 session A 執行 rollback 語句回滾的時候,session C 幾乎同時發現死鎖並返回。這個死鎖產生的邏輯是這樣的:

三、狀態變化圖 -- 死鎖

 

 

 

圖 7 惟一鍵衝突 -- 死鎖

在 session A 執行 rollback 語句回滾的時候,session C 幾乎同時發現死鎖並返回。

這個死鎖產生的邏輯是這樣的:

insert into t values(11,10,10) on duplicate key update d=100; 

1. 在 T1 時刻,啓動 session A,並執行 insert 語句,此時在索引 c 的 c=5 上加了記錄鎖。注意,這個索引是惟一索引,所以退化爲記錄鎖(若是你的印象模糊了,能夠回顧
下第 21 篇文章介紹的加鎖規則)。
2. 在 T2 時刻,session B 要執行相同的 insert 語句,發現了惟一鍵衝突,加上讀鎖;一樣地,session C 也在索引 c 上,c=5 這一個記錄上,加了讀鎖。
3. T3 時刻,session A 回滾。這時候,session B 和 session C 都試圖繼續執行插入操做,都要加上寫鎖。兩個 session 都要等待對方的行鎖,因此就出現了死鎖。

這個流程的狀態變化圖以下所示。

 

圖 8 狀態變化圖 -- 死鎖

5、insert into … on duplicate key update

上面這個例子是主鍵衝突後直接報錯,若是是改寫成的話,就會給索引 c 上 (5,10] 加一個排他的 next-key lock(寫鎖)。

nsert into … on duplicate key update 這個語義的邏輯是,插入一行數據,若是碰到惟一鍵約束,就執行後面的更新語句。

注意,若是有多個列違反了惟一性約束,就會按照索引的順序,修改跟第一個索引衝突的行。

如今表 t 裏面已經有了 (1,1,1) 和 (2,2,2) 這兩行,咱們再來看看下面這個語句執行的效果:

圖 9 兩個惟一鍵同時衝突

能夠看到,主鍵 id 是先判斷的,MySQL 認爲這個語句跟 id=2 這一行衝突,因此修改的是 id=2 的行。

須要注意的是,執行這條語句的 affected rows 返回的是 2,很容易形成誤解。實際上,真正更新的只有一行,只是在代碼實現上,insert 和 update 都認爲本身成功了,update
計數加了 1, insert 計數也加了 1。

6、小結

今天這篇文章,我和你介紹了幾種特殊狀況下的 insert 語句。

  1. insert … select 是很常見的在兩個表之間拷貝數據的方法。你須要注意,在可重複讀隔離級別下,這個語句會給 select 的表裏掃描到的記錄和間隙加讀鎖。
  2. 而若是 insert 和 select 的對象是同一個表,則有可能會形成循環寫入。這種狀況下,咱們須要引入用戶臨時表來作優化。
  3. insert 語句若是出現惟一鍵衝突,會在衝突的惟一值上加共享的 next-key lock(S 鎖)。所以,碰到因爲惟一鍵約束致使報錯後,要儘快提交或回滾事務,避免加鎖時間過長。

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

你平時在兩個表之間拷貝數據用的是什麼方法,有什麼注意事項嗎?在你的應用場景裏,這個方法,相較於其餘方法的優點是什麼呢?

你能夠把你的經驗和分析寫在評論區,我會在下一篇文章的末尾選取有趣的評論來和你一塊兒分析。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。

7、上期問題時間

咱們已經在文章中回答了上期問題。

有同窗提到,若是在 insert … select 執行期間有其餘線程操做原表,會致使邏輯錯誤。其實,這是不會的,若是不加鎖,就是快照讀。

一條語句執行期間,它的一致性視圖是不會修改的,因此即便有其餘事務修改了原表的數據,也不會影響這條語句看到的數據。

評論區留言點贊板:

@長傑 同窗回答得很是準確。

相關文章
相關標籤/搜索