MySQL中由load data語句引起死鎖的解決案例
一個線上項目報的死鎖,簡要說明一下產(chǎn)生原因、處理方案和相關(guān)的一些點.
1、背景
這是一個類似數(shù)據(jù)分析的項目,數(shù)據(jù)完全通過LOAD DATA語句導(dǎo)入一個InnoDB表中。為方便描述,表結(jié)構(gòu)簡化為如下:
Create table tb(id int primary key auto_increment, c int not null) engine=innodb;
導(dǎo)入數(shù)據(jù)的語句對應(yīng)為
Load data infile ‘data1.csv' into table tb; Load data infile ‘data2.csv' into table tb;
cat Data1.csv 1 100 2 100 3 100 Cat data2.csv 10 100 11 100 12 100
產(chǎn)生死鎖的證據(jù)是在show engine innodb status的LATEST DETECTED DEADLOCK段中看到死鎖信息,簡化為如下:
說明
從上面表格中看出,事務(wù)1在等待某一行的鎖。而事務(wù)2持有這行的鎖,但等待表的自增鎖(AUTO_INC),判斷為死鎖,事務(wù)回滾。
這里事務(wù)1沒有寫出來,但是可以推斷,事務(wù)1持有這個表的自增鎖(否則就不是死鎖了)。
2、背景知識1:AUTO_INC lock 及其選項
在InnoDB表中,若存在自增字段,則會維護一個表級別的鎖,這里稱為自增鎖。每次插入新數(shù)據(jù),或者update語句修改了此字段,都會需要獲取這個鎖
由于一個事務(wù)可能包含多個語句,而并非所有的語句都與自增字段有關(guān),因此InnoDB作了一個特殊的處理,自增鎖在一個語句結(jié)束后馬上被釋放。之所以說是特殊處理,是因為普通的鎖,都是在事務(wù)結(jié)束后釋放。
若一個表有自增字段,一個insert語句不指定該字段的值,或指定為NULL時,InnoDB會給它賦值為當(dāng)前的AUTO_INCREMENT的值,然后AUTO_INCREMENT加1。
與這個自增鎖相關(guān)的一個參數(shù)是innodb_autoinc_lock_mode. 默認(rèn)值為1,可選為0,1,2。
我們先來看當(dāng)這個值設(shè)置為0時,一個有自增字段的表,插入一行數(shù)據(jù)時的行為:
1) 申請AUTO_INC鎖
2) 得到當(dāng)前AUTO_INCREMNT值n,給AUTO_INCREMENT 加1
3) 執(zhí)行插入操作,并將n填入新增的行對應(yīng)字段中
4) 釋放AUTO_INC鎖
我們看到這個過程中,雖然InnoDB為了減少鎖粒度,在語句執(zhí)行完成就馬上釋放,但這鎖還是太大了――它包括了插入操作的時間。這就導(dǎo)致了兩個insert語句,實際上沒辦法并行。
沒有這個參數(shù)之前,行為就是與設(shè)置為0相同,0這個選項就是留著兼容的。
很容易想到設(shè)置為1的時候,應(yīng)該是將3) 和 4)對調(diào)。但是本文還是要討論為0的情況,因為我們的前提是LOAD語句,而LOAD語句這類插入多行的語句中(包括insert …select …),即使設(shè)置為1也沒用,會退化為0的模式。
3、背景知識2:LOAD DATA語句的主從行為
為什么插入多行的語句要即使將innodb_autoinc_lock_mode設(shè)置為1,也會用0的模式呢?
主要原因還是為了主從一致性。設(shè)想binlog_format='statement',一個LOAD DATA語句在主庫的binlog直接記錄為語句本身,那從庫如何重放:
1) 將load data用到的文件發(fā)給slave,slave將文件保存在臨時目錄。
2) 在slave也執(zhí)行一次LOAD DATA語句。
其間有一個問題:slave怎么保證load data語句的自增id字段與master相同?
為了解決這個問題,主庫的binlog中還有一個set SET INSERT_ID命令,表明這個LOAD DATA語句插入的第一行的自增ID值。這樣slave在執(zhí)行l(wèi)oad data之前,先執(zhí)行了這個set SET INSERT_ID語句,用于保證執(zhí)行結(jié)果與主庫一模一樣。
上述的機制能保證主從數(shù)據(jù)一致的前提是:主從庫上LOAD DATA語句生成的自增ID值必須是連續(xù)的。
4、背景知識1+2:分析
回到前面說的模式0和1的區(qū)別,我們看到,如果AUTO_INC鎖在整個語句開始之前就獲取,在語句結(jié)束之后才釋放,這樣就能保證整個語句生成的id連續(xù)――模式0的保證。
對于1,每次拿到下一個值就釋放,插入數(shù)據(jù)后,若需要再申請,則不連續(xù)。
這就是為什么,即使設(shè)置為1,對于多行操作,會退化成0。
至此我們知道這個死鎖出現(xiàn)的原因,是這兩個LOAD DATA語句不僅會訪問相同的記錄,還會訪問同一個AUTO_INC鎖,造成互相等待。
到此沒完,因為我們知道雖然兩個線程訪問兩個鎖可能造成死鎖,但是死鎖還有另外一個條件,與申請順序有關(guān)。既然AUTO_INC是一個表鎖,不論誰先拿到,會阻塞其他同表的LOAD DATA的執(zhí)行,又為什么會在某個記錄上出現(xiàn)鎖等待?
5、背景知識3:AUTO_INC的加鎖時機
前面我們說到每次涉及到插入新數(shù)據(jù),就會要求對AUTO_INC加鎖,并列出了流程。但這個流程是對于需要從InnoDB中得到自增值來設(shè)置列值的情況。另一種情況是在語句中已經(jīng)指定了該列的值。
比如對于這個表,執(zhí)行 insert into tb values(9,100). 此時id的值已經(jīng)明確是9,雖然不需要取值來填,但是插入這行后有可能需要改變AUTO_INCREMENT的值(若原來是<10,則應(yīng)該改為10),所以這個鎖還是省不了。流程變成:
1) 插入數(shù)據(jù)
2) 若失敗則流程結(jié)束
3) 若成功,申請AUTO_INC鎖
4) 調(diào)用set_max….函數(shù),如有必要則修改AUTO_INCREMENT
5) 語句結(jié)束時釋放AUTO_INC鎖。
6、為什么修改AUTO_INC順序
這么調(diào)整的好處是什么? 主要是為了減少不必要的鎖訪問。若在插入數(shù)據(jù)期間發(fā)生錯誤,比如其他字段造成DUPLICATE KEY error,這樣就不用訪問AUTO_INC鎖。
7、死鎖過程復(fù)現(xiàn)
必須強調(diào)是“語句結(jié)束時”。這樣我們來看一個每行都已經(jīng)指定了自增列值的LOAD DATA語句的流程(也就是本文例子的情況):
1) 插入第一條數(shù)據(jù)
2) 申請AUTO_INC鎖
3) 插入第二條
4) 申請AUTO_INC 鎖(因為已經(jīng)是自己的,直接成功)
5) 。。。。。。插入剩余所有行
6) 釋放AUTO_INC鎖。
所以這個流程就簡單描述為:插入第一行,申請AUTO_INC鎖,然后插入剩下的所有行后再釋放。
我們前面提到過,插入第一條數(shù)據(jù)時可能需要訪問的記錄鎖,是要等到整個事務(wù)結(jié)束后才釋放的.
有了上面的這些背景知識,我們來復(fù)現(xiàn)一下死鎖出現(xiàn)的過程
可以看到觸發(fā)條件還是比較苛刻的,尤其是session2要剛好要用到session1鎖住的那個記錄鎖。需要說明,由于InnoDB內(nèi)部對記錄的表示,同一個記錄鎖并不表示主鍵值一定相同。
8、解決方案1:去掉不必要的AUTO_INCREMENT字段
在這個業(yè)務(wù)中,由于所有的數(shù)據(jù)都是通過LOAD DATA進去,而且都已經(jīng)指定了自增字段的值,因此這個AUTO)INCREMENT屬性是不需要的。
少了一個,就死鎖不了了。
9、解決方案2:強制模式1
前面我們說到innodb_autoinc_lock_mode這個參數(shù)的可選值有0、1、2。當(dāng)設(shè)置為1的時候,在LOAD DATA語句會退化為模式0。但若設(shè)置為2,則無論如何都會使用模式1。
我們前面說到使用模式1會導(dǎo)致LOAD DATA生成的自增id值不連續(xù),這樣會導(dǎo)致在binlog_format是1時主從不一致,因此設(shè)置為2的前提,是binlog_format 是row.
在binlog_format='row'時,設(shè)置innodb_autoinc_lock_mode為2是安全的。
若允許,方案2比方案1更輕量些,不需要修改數(shù)據(jù)和表結(jié)構(gòu)。
- mysql遇到load data導(dǎo)入文件數(shù)據(jù)出現(xiàn)1290錯誤的解決方案
- Java利用MYSQL LOAD DATA LOCAL INFILE實現(xiàn)大批量導(dǎo)入數(shù)據(jù)到MySQL
- mysql中Load Data記錄換行問題的解決方法
- mysql load data infile 的用法(40w數(shù)據(jù) 用了3-5秒導(dǎo)進mysql)
- mysql中的Load data的使用方法
- mysql 的load data infile
- mysql Load Data InFile 的用法
- MySQL數(shù)據(jù)庫 Load Data 多種用法
相關(guān)文章
Navicat連接linux虛擬機上的MySQL可能遇到的問題及排查方法
這篇文章主要介紹了Navicat連接linux虛擬機上的MySQL可能遇到的問題以及如何排查,本文給大家展示了問題描述及解決方法,需要的朋友可以參考下2024-02-02MySql Group By對多個字段進行分組的實現(xiàn)方法
這篇文章主要介紹了MySql Group By對多個字段進行分組的實現(xiàn)方法,需要的朋友可以參考下2017-09-09MySQL 與 Elasticsearch 數(shù)據(jù)不對稱問題解決辦法
這篇文章主要介紹了MySQL 與 Elasticsearch 數(shù)據(jù)不對稱問題解決辦法的相關(guān)資料,對于 elasticsearch 增量寫入,但經(jīng)常jdbc源一端的數(shù)據(jù)庫可能會做數(shù)據(jù)庫刪除或者更新操作,這里提供解決辦法,需要的朋友可以參考下2017-08-08通過MySQL日志實時查看執(zhí)行語句以及更新日志的教程
這篇文章主要介紹了通過MySQL日志實時查看執(zhí)行語句以及更新日志的教程,文中所講的方法使用到了mysqladmin命令,需要的朋友可以參考下2015-12-12mysql出現(xiàn)ERROR 1819 (HY000)的解決方法
這篇文章主要為大家詳細(xì)介紹了mysql出現(xiàn)ERROR 1819 (HY000)的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10MySQL在Windows中net start mysql 啟動MySQL服務(wù)報錯 發(fā)生系統(tǒng)錯誤解決方案
這篇文章主要介紹了MySQL在Windows中net start mysql 啟動MySQL服務(wù)報錯 發(fā)生系統(tǒng)錯誤解決方案,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07C#如何在海量數(shù)據(jù)下的高效讀取寫入MySQL
這篇文章主要介紹了C#如何在海量數(shù)據(jù)下的高效讀取寫入MySQL的相關(guān)資料,需要的朋友可以參考下2016-12-12