修改Lower_Case_Table_Names 導(dǎo)致 Frm 文件刪除失敗
最近碰到一個(gè)線上問(wèn)題,mysqldump 導(dǎo)出數(shù)據(jù)報(bào)錯(cuò):
經(jīng)過(guò)分析發(fā)現(xiàn),報(bào)錯(cuò)信息中的數(shù)據(jù)庫(kù),所有??表名?
?都混用了大小寫(xiě)字母,因?yàn)閯?chuàng)建表之后,系統(tǒng)變量 ??lower_case_table_names?
? 的值被從 0 修改為 1,導(dǎo)致刪除這個(gè)數(shù)據(jù)庫(kù)時(shí),每個(gè)表的 ??ibd?
? 文件刪除成功,??frm?
? 文件刪除失敗。
本文我們就來(lái)聊聊這個(gè) mysqldump 問(wèn)題產(chǎn)生的原因,以及在刪除數(shù)據(jù)庫(kù)的過(guò)程中,??lower_case_table_names?
? 是怎么影響 frm、ibd 文件的刪除邏輯的。
本文內(nèi)容基于 MySQL 5.7.35 源碼,涉及存儲(chǔ)引擎為 InnoDB。
1、問(wèn)題復(fù)現(xiàn)
我們先通過(guò)幾個(gè)步驟,來(lái)復(fù)現(xiàn) mysqldump 問(wèn)題的產(chǎn)生過(guò)程。
第 1 步,確認(rèn)系統(tǒng)變量 lower_case_table_names 的值是 0:
第 2 步,創(chuàng)建測(cè)試數(shù)據(jù)庫(kù)、表:
第 3 步,查看 test6 數(shù)據(jù)庫(kù)目錄下的文件:
第 4 步,修改 MySQL 配置文件,把系統(tǒng)變量 lower_case_table_names 的值修改為 1,然后重啟 MySQL。
第 5 步,重新連接 MySQL,確認(rèn)系統(tǒng)變量 lower_case_table_names 的值是 1:
第 6 步,在 lower_case_table_names = 1 的場(chǎng)景下,刪除測(cè)試庫(kù):
報(bào)錯(cuò)信息說(shuō)明不能刪除 ./test6 目錄,這是因?yàn)?test6 目錄下還有 frm 文件:
從上面的結(jié)果可以看到,db.opt、Test.ibd 都已經(jīng)刪除,只剩下 Test.frm。
InnoDB 刪除表時(shí),會(huì)先把表的元數(shù)據(jù)從 information_schema 庫(kù)的 INNODB_SYS_TABLESPACES、INNODB_SYS_TABLES、INNODB_SYS_COLUMNS、INNODB_SYS_INDEXES 等數(shù)據(jù)字典表中刪除,最后才會(huì)刪除 ibd 文件。
刪除表的過(guò)程中,Test.ibd 文件被刪除了,就說(shuō)明 Test 表被成功刪除了。Test.frm 文件雖然還在,但已經(jīng)沒(méi)有實(shí)際用處了。
此時(shí),通過(guò) show tables 還能列出測(cè)試庫(kù) test6 中的 Test 表:
show tables 會(huì)掃描數(shù)據(jù)庫(kù)目錄,獲取其中的 frm 文件名(不含 .frm 后綴),并根據(jù) lower_case_table_names 的值,把 frm 文件名轉(zhuǎn)換為相應(yīng)的大小寫(xiě)形式,作為該 frm 文件對(duì)應(yīng)的表名。
因?yàn)?test6 的數(shù)據(jù)庫(kù)目錄中還存在 Test.frm 文件,所以執(zhí)行結(jié)果中能看到 Test 表,但這并不表示 Test 表還存在,通過(guò)以下 SQL 可以驗(yàn)證:
從上面的執(zhí)行結(jié)果可以看到,InnoDB 的數(shù)據(jù)字典表中,已經(jīng)沒(méi)有測(cè)試庫(kù) test6 的表了。
第 7 步,導(dǎo)出數(shù)據(jù):
到這里,我們就已經(jīng)復(fù)現(xiàn)出來(lái) mysqldump 導(dǎo)出數(shù)據(jù)報(bào)錯(cuò)的問(wèn)題了。
為什么報(bào)錯(cuò)信息里的表名不是 Test,而是 test?
這是因?yàn)?lower_case_table_names = 1 時(shí),MySQL 內(nèi)部會(huì)使用小寫(xiě)形式的表名,具體請(qǐng)看后面關(guān)于 lower_case_table_names 的介紹。
2、解決方案
如果只想臨時(shí)解決 mysqldump 導(dǎo)出數(shù)據(jù)問(wèn)題,可以通過(guò) --databases 指定需要導(dǎo)出的數(shù)據(jù)庫(kù):
如果想一勞永逸的解決問(wèn)題,直接把已刪除數(shù)據(jù)庫(kù)的殘留目錄刪掉就可以了。
還是以前面的測(cè)試數(shù)據(jù)庫(kù) test6 為例,因?yàn)橐呀?jīng)通過(guò) DROP DATABASE 對(duì) test6 進(jìn)行了刪除操作,該數(shù)據(jù)庫(kù)中的所有表都已經(jīng)被刪除了。
test6 目錄還在,是因?yàn)楸淼?frm 文件沒(méi)有被刪除,這些 frm 文件也沒(méi)有實(shí)際用處了,此時(shí),test6 目錄屬于殘留目錄,可以刪除。
為了保險(xiǎn)起見(jiàn),可以先把殲留目錄移動(dòng)到其它目錄下暫存,確認(rèn) MySQL 一切正常之后,再刪除殘留目錄。
3、lower_case_table_names
系統(tǒng)變量 lower_case_table_names 會(huì)影響數(shù)據(jù)庫(kù)名、數(shù)據(jù)庫(kù)目錄名、表名、frm 文件名、ibd 文件名,它有 3 種取值(0、1、2),接下來(lái)詳細(xì)介紹。
(1)lower_case_table_names = 0
lower_case_table_names = 0,Linux、Unix 的默認(rèn)值,表示數(shù)據(jù)庫(kù)名、表名區(qū)分大小寫(xiě):
- server 層的數(shù)據(jù)庫(kù)名 & 目錄名、InnoDB 數(shù)據(jù)字典表中存放的數(shù)據(jù)庫(kù)名是CREATE DATABASE 中指定的數(shù)據(jù)庫(kù)名。
- frm & ibd 文件名、InnoDB 數(shù)據(jù)字典表中存放的表名是CREATE TABLE 中指定的表名。
lower_case_table_names = 0 時(shí),創(chuàng)建測(cè)試數(shù)據(jù)庫(kù)、表:
查看數(shù)據(jù)庫(kù)目錄名、表的 frm、ibd 文件名:
server 層通過(guò)表名去 InnoDB 中查找對(duì)應(yīng)的表時(shí),也會(huì)區(qū)分大小寫(xiě):
從示例 SQL 可以看到,只有指定正確的大小寫(xiě),SQL 才能執(zhí)行成功,否則都會(huì)報(bào)錯(cuò)說(shuō)表不存在。
通過(guò)以下 SQL 也可以驗(yàn)證,存放在 InnoDB 數(shù)據(jù)字典中的數(shù)據(jù)庫(kù)名、表名是 CREATE DATABASE、CREATE TABLE 中指定的數(shù)據(jù)庫(kù)名、表名:
(2)lower_case_table_names = 1
lower_case_table_names = 1,Windows 的默認(rèn)值,表示數(shù)據(jù)庫(kù)名、表名都不區(qū)分大小寫(xiě):
- server 層的數(shù)據(jù)庫(kù)名 & 目錄名、InnoDB 數(shù)據(jù)字典表中存放的數(shù)據(jù)庫(kù)名是CREATE DATABASE 中指定數(shù)據(jù)庫(kù)名的小寫(xiě)形式。
- frm & ibd 文件名、 InnoDB 數(shù)據(jù)字典表中存放的表名是CREATE TABLE 中指定表名的小寫(xiě)形式。
lower_case_table_names = 1 時(shí),創(chuàng)建測(cè)試數(shù)據(jù)庫(kù)、表:
查看數(shù)據(jù)庫(kù)目錄名、表的 frm、ibd 文件名,全部被轉(zhuǎn)換為小寫(xiě)了:
server 層通過(guò)表名去 InnoDB 查找對(duì)應(yīng)的表之前,也會(huì)把表名轉(zhuǎn)換為小寫(xiě)形式:
從示例 SQL 可以看到,表名包含大小寫(xiě)字母、全部是小寫(xiě)字母,SQL 都能執(zhí)行成功。
通過(guò)以下 SQL 也可以驗(yàn)證,存放在 InnoDB 數(shù)據(jù)字典中的數(shù)據(jù)庫(kù)名、表名都轉(zhuǎn)換為小寫(xiě)形式了:
(3)lower_case_table_names = 2
lower_case_table_names = 2,這是 MacOS 的默認(rèn)值,這個(gè)選項(xiàng)值的情況比前面兩種復(fù)雜一些:
- 數(shù)據(jù)庫(kù)名、數(shù)據(jù)庫(kù)目錄名是CREATE DATABASE 中指定的數(shù)據(jù)庫(kù)名。
- 表的 frm 文件名是CREATE TABLE 中指定的表名。
- 表的 ibd 文件名是CREATE TABLE 中指定表名的小寫(xiě)形式。
- InnoDB 數(shù)據(jù)字典表中存放的數(shù)據(jù)庫(kù)名、表名小寫(xiě)形式。
?上面 4 條可以歸納為 2 條:
- server 層使用 CREATE DATABASE、CREATE TABLE 中指定的數(shù)據(jù)庫(kù)名、表名。
- InnoDB 使用 CREATE DATABASE、CREATE TABLE 中指定數(shù)據(jù)庫(kù)名、表名的小寫(xiě)形式。
lower_case_table_names = 2 時(shí),創(chuàng)建測(cè)試數(shù)據(jù)庫(kù)、表:
查看數(shù)據(jù)庫(kù)目錄名、表的 frm、ibd 文件名:
數(shù)據(jù)庫(kù)目錄由 server 層創(chuàng)建,目錄名是 CREATE DATABASE 中指定的數(shù)據(jù)庫(kù)名。
frm 文件由 server 層創(chuàng)建,文件名是 CREATE TABLE 中指定的表名。
ibd 文件由 InnoDB 創(chuàng)建,文件名是 CREATE TABLE 中指定表名的小寫(xiě)形式。
server 層通過(guò)表名去 InnoDB 查找對(duì)應(yīng)的表之前,也會(huì)把表名轉(zhuǎn)換為小寫(xiě)形式:
從示例 SQL 可以看到,表名包含大小寫(xiě)字母、全部是小寫(xiě)字母,SQL 都能執(zhí)行成功。
通過(guò)以下 SQL 也可以驗(yàn)證,存放在 InnoDB 數(shù)據(jù)字典中的數(shù)據(jù)庫(kù)名、表名都轉(zhuǎn)換為小寫(xiě)形式了:
4、為什么 frm 文件會(huì)刪除失敗?
我們先來(lái)回顧一下 frm 文件刪除失敗的場(chǎng)景:
- lower_case_table_names = 0 時(shí),創(chuàng)建了數(shù)據(jù)庫(kù)和表(表名包含大小寫(xiě)字母)。
- lower_case_table_names = 1 時(shí),刪除數(shù)據(jù)庫(kù),ibd 文件刪除成功,frm 文件刪除失敗。
我們還是以 1. 問(wèn)題復(fù)現(xiàn)中的測(cè)試數(shù)據(jù)庫(kù)、表為例,lower_case_table_names = 0 時(shí),創(chuàng)建測(cè)試數(shù)據(jù)庫(kù)、表之后,frm、ibd 文件如下:
3.1 lower_case_table_names = 1 小節(jié)介紹過(guò),lower_case_table_names 修改為 1 之后,server 層通過(guò)表名去 InnoDB 查找對(duì)應(yīng)的表之前,會(huì)把表名轉(zhuǎn)換為小寫(xiě)形式。
接下來(lái),我們先來(lái)看看刪除數(shù)據(jù)庫(kù)的主要邏輯:
第 1 步,遍歷待刪除數(shù)據(jù)庫(kù)的目錄,找到該目錄下所有的 frm 文件,把 frm 文件名(不含 .frm? 后綴)轉(zhuǎn)換為小寫(xiě)?,作為表名。
以 test6 數(shù)據(jù)庫(kù)為例:
遍歷 test6 目錄,找到該目錄下的 frm 文件,該目錄下只有一個(gè) frm 文件:Test.frm。
把 frm 文件名轉(zhuǎn)換為小寫(xiě),得到表名 test。
第 2 步,執(zhí)行第一種刪表邏輯:以第 1 步中根據(jù) frm 文件名得到的表名執(zhí)行刪表操作,由 InnoDB 和 server 層共同完成,InnoDB 負(fù)責(zé)刪除表的元數(shù)據(jù)和 ibd 文件,server 層負(fù)責(zé)刪除 frm 文件。
遍歷第 1 步得到的表名?,加上 .frm 后綴,得到 frm 文件名,然后根據(jù) frm 文件是否存在執(zhí)行不同的邏輯。
如果 frm 文件存在?,則調(diào)用 InnoDB 的刪表方法,從 InnoDB 數(shù)據(jù)字典表中刪除該表的元數(shù)據(jù),以及刪除 ibd 文件。
InnoDB 刪表成功之后,server 層會(huì)刪除該表的 frm 文件;InnoDB 刪表失敗,server 層會(huì)記錄第一種刪表邏輯中存在刪除失敗的表。
如果 frm 文件不存在,不會(huì)調(diào)用 InnoDB 的刪表方法,server 層也會(huì)記錄第一種刪表邏輯中存在刪除失敗的表。
以測(cè)試數(shù)據(jù)庫(kù) test6 為例:
第 1 步得到的表名為 test?,加上 .frm? 后綴,得到文件名:test.frm。
Linux 系統(tǒng)的文件名是區(qū)分大小寫(xiě)的,test6 目錄下只存在 Test.frm,用 test.frm 無(wú)法匹配 Test.frm 文件,也就是說(shuō),test.frm 文件不存在。
因?yàn)?test6 目錄下不存在 test.frm 文件,server 層會(huì)記錄第一種刪表邏輯中存在刪除失敗的表。
第 3 步,判斷第 2 步是否存在刪除失敗的表。
如果存在刪除失敗的表,會(huì)執(zhí)行第二種?刪表邏輯,由 InnoDB 獨(dú)自完成:從 ?information_schema.INNODB_SYS_TABLES? 中獲取要?jiǎng)h除的數(shù)據(jù)庫(kù)中的表名,逐個(gè)執(zhí)行刪表操作。
從 INNODB_SYS_TABLES 中獲取表名,以及刪表操作都在 InnoDB 中進(jìn)行,不會(huì)受到 lower_case_table_names 的影響。
以 test6 數(shù)據(jù)庫(kù)為例,第二種刪表邏輯如下:
① 從 INNODB_SYS_TABLES 表獲取 test6 數(shù)據(jù)庫(kù)中未被刪除的第一個(gè)表名。
② 把該表的元數(shù)據(jù)信息從對(duì)應(yīng)的數(shù)據(jù)字典表中刪除。
③ 刪除該表的 ibd 文件。
循環(huán) ① ~ ③,直到 test6 中的所有表都被刪除之后,第二種刪表邏輯結(jié)束。
介紹完刪除數(shù)據(jù)庫(kù)的邏輯,我們來(lái)總結(jié)一下:為什么 frm 文件會(huì)刪除失敗?
lower_case_table_names 的值從 0 修改為 1 之后,第一種刪表邏輯,因?yàn)楸砻拇笮?xiě)問(wèn)題,導(dǎo)致找不到 frm 文件,執(zhí)行失敗,轉(zhuǎn)而執(zhí)行第二種刪表邏輯。
第二種刪表邏輯,只會(huì)從 InnoDB 數(shù)據(jù)字典表中刪除表的元數(shù)據(jù),然后刪除表的 ibd 文件,不包含刪除 frm 文件的操作,frm 文件也就不會(huì)被刪了。
5、為什么 ibd 文件能刪除成功?
通過(guò) 4. 為什么 frm 文件會(huì)刪除失敗?小節(jié)的介紹,我們可以看到,第一種刪表邏輯,由于找不到表的 frm 文件,不會(huì)觸發(fā) InnoDB 的刪表操作,也就不會(huì)刪除 ibd 文件了。
第二種刪表邏輯,先從 INNODB_SYS_TABLES 表中獲取表名,然后通過(guò)表名找表對(duì)應(yīng)的表空間,表空間信息中包含從 INNODB_SYS_DATAFILES 表中讀取到的 ibd 文件路徑。
刪除 ibd 文件時(shí),會(huì)從表空間信息中獲取 ibd 文件路徑。
ibd 文件能刪除成功,取決于以下 2 個(gè)因素:
- 第二種刪表邏輯,從INNODB_SYS_TABLES 中獲取表名之后,不會(huì)進(jìn)行大小寫(xiě)轉(zhuǎn)換(也就是不會(huì)受到 lower_case_table_names 的影響),而是直接以獲取到的表名,加載表的元數(shù)據(jù)信息。
- 創(chuàng)建表時(shí)寫(xiě)入INNODB_SYS_DATAFILES 表中的 ibd 文件路徑,不管系統(tǒng)變量 lower_case_table_names 的值修改成什么,該表中存放的 ibd 文件路徑都不會(huì)變。
6、總結(jié)
如果程序代碼中已經(jīng)使用了某個(gè)數(shù)據(jù)庫(kù)的表,或者 MySQL 實(shí)例已經(jīng)在線上正式使用,最好不要修改 lower_case_table_names 的值,否則,可能會(huì)造成意想不到的問(wèn)題。
本文轉(zhuǎn)載自微信公眾號(hào)「一樹(shù)一溪」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系一樹(shù)一溪公眾號(hào)。