在上一篇文章中,我提到 MySQL 對自增主鍵鎖作了優化,儘可能在申請到自增 id 之後,就釋放自增鎖。bash
所以,insert 語句是一個很輕量的操做。不過,這個結論對於「普通的 insert 語句」纔有效。也就是說,還有些 insert 語句是屬於「特殊狀況」的,在執行過程當中須要給其餘資源
加鎖,或者沒法在申請到自增 id 之後就立馬釋放自增鎖。session
那麼,今天這篇文章,咱們就一塊兒來聊聊這個話題。併發
咱們先從昨天的問題提及吧。表 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 中,出現主備不一致。
固然了,執行 insert … select 的時候,對目標表也不是鎖全表,而是隻鎖住須要訪問的資源。
若是如今有這麼一個需求:要往表 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 中的話:
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 的結果來「腦補」整條語句的執行過程。今天,咱們就來一塊兒試試
如圖 4 所示就是這條語句的 explain 結果。
圖 4 explain 結果
從 Extra 字段能夠看到「Using temporary」字樣,表示這個語句用到了臨時表。也就是說,執行過程當中,須要把表 t 的內容讀出來,寫入臨時表。
圖中 rows 顯示的是 1,咱們不妨先對這個語句的執行流程作一個猜想:若是說是把子查詢的結果讀出來(掃描 1 行),寫入臨時表,而後再從臨時表讀出來(掃描 1 行),寫回
表 t 中。那麼,這個語句的掃描行數就應該是 2,而不是 5。
因此,這個猜想不對。實際上,Explain 結果裏的 rows=1 是由於受到了 limit 1 的影響。
從另外一個角度考慮的話,咱們能夠看看 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;
前面的兩個例子是使用 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 狀態變化圖 -- 死鎖
上面這個例子是主鍵衝突後直接報錯,若是是改寫成的話,就會給索引 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。
今天這篇文章,我和你介紹了幾種特殊狀況下的 insert 語句。
最後,我給你留一個問題吧。
你平時在兩個表之間拷貝數據用的是什麼方法,有什麼注意事項嗎?在你的應用場景裏,這個方法,相較於其餘方法的優點是什麼呢?
你能夠把你的經驗和分析寫在評論區,我會在下一篇文章的末尾選取有趣的評論來和你一塊兒分析。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。
咱們已經在文章中回答了上期問題。
有同窗提到,若是在 insert … select 執行期間有其餘線程操做原表,會致使邏輯錯誤。其實,這是不會的,若是不加鎖,就是快照讀。
一條語句執行期間,它的一致性視圖是不會修改的,因此即便有其餘事務修改了原表的數據,也不會影響這條語句看到的數據。
評論區留言點贊板:
@長傑 同窗回答得很是準確。