前言
在MySQL 8.0之前的版本中,由于架構(gòu)的原因忿磅,MySQL在server層使用統(tǒng)一的frm文件來存儲表元數(shù)據(jù)信息,這個信息能夠被不同的存儲引擎識別。而實(shí)際上InnoDB本身也存儲有元數(shù)據(jù)信息崔拥。
這給ddl帶來了一定的挑戰(zhàn),因?yàn)檫@種架構(gòu)無法做到ddl的原子化凤覆,我們在線上經(jīng)常能夠看到數(shù)據(jù)目錄下遺留的臨時文件链瓦,或者類似server層和innodb層列個數(shù)不一致之類的錯誤。甚至某些ddl可能還遺留元數(shù)據(jù)在innodb內(nèi)盯桦,而丟失了frm慈俯,導(dǎo)致無法重建表…..(我們?yōu)榱私鉀Q這個問題,實(shí)現(xiàn)了一個叫drop table force的功能拥峦,去強(qiáng)制做清理….)
(以下所有的討論都假定使用InnoDB存儲引擎)
到了8.0版本贴膘,我們知道所有的元數(shù)據(jù)已經(jīng)統(tǒng)一用InnoDB來進(jìn)行管理,這就給實(shí)現(xiàn)原子ddl帶來了可能略号,幾乎所有的對innodb表刑峡,存儲過程洋闽,觸發(fā)器,視圖或者UDF的操作突梦,都能做到原子化:
- 元數(shù)據(jù)修改诫舅,binlog以及innodb的操作都放在一個事務(wù)中
- 增加了一個內(nèi)部隱藏的系統(tǒng)表`mysql.innodb_ddl_log`,ddl操作被記錄到這個表中阳似,注意對該表的操作產(chǎn)生的redo會fsync到磁盤上骚勘,而不會考慮innodb_flush_log_at_trx_commit的配置。當(dāng)崩潰重啟時撮奏,會根據(jù)事務(wù)是否提交來決定通過這張表的記錄去回滾或者執(zhí)行ddl操作
- 增加了一個post-ddl的階段俏讹,這也是ddl的最后一個階段,會去:1. 真正的物理刪除或重命名文件; 2. 刪除innodb_ddl_log中的記錄項; 3.對于一些ddl操作還會去更新其動態(tài)元數(shù)據(jù)信息(存儲在`mysql.innodb_dynamic_metadata`畜吊,例如corrupt flag, auto_inc值等)
- 一個正常運(yùn)行的ddl結(jié)束后泽疆,其ddl log也應(yīng)該被清理,如果這中間崩潰了玲献,重啟時會去嘗試重放:1.如果已經(jīng)走到最后一個ddl階段的(commit之后)殉疼,就replay ddl log,把ddl完成掉捌年;2. 如果處于某個中間態(tài)瓢娜,則回滾ddl
由于引入了atomic ddl, 有些ddl操作的行為也發(fā)生了變化:
- DROP TABLE: 在之前的版本中礼预,一個drop table語句中如果要刪多個表眠砾,比如t1,t2, t2不存在時,t1會被刪除托酸。但在8.0中褒颈,t1和t2都不會被刪除,而是拋出錯誤励堡。因此要注意5.7->8.0的復(fù)制問題 (DROP VIEW, CREATE USER也有類似的問題)
- DROP DATABASE: 修改元數(shù)據(jù)和ddl_log先提交事務(wù)应结,而真正的物理刪除數(shù)據(jù)文件放在最后刨疼,因此如果在刪除文件時崩潰,重啟時會根據(jù)ddl_log繼續(xù)執(zhí)行drop database
測試:
MySQL很貼心的加了一個選項
innodb_print_ddl_logs
鹅龄,打開后我們可以從錯誤日志看到對應(yīng)的ddl log币狠,下面我們通過這個來看下一些典型ddl的過程
root@(none) 11:12:19>SET GLOBAL innodb_print_ddl_logs = 1;
Query OK, 0 rows affected (0.00 sec)
root@(none) 11:12:22>SET GLOBAL log_error_verbosity = 3;
Query OK, 0 rows affected (0.00 sec)
CREATE DATABASE
mysql> CREATE DATABASE test;
Query OK, 1 row affected (0.02 sec)
創(chuàng)建數(shù)據(jù)庫語句沒有寫log_ddl,可能覺得這不是高頻操作砾层,如果創(chuàng)建database的過程中失敗了漩绵,重啟后可能需要手動刪除目錄。
CREATE TABLE
mysql> USE test;
Database changed
mysql> CREATE TABLE t1 (a INT PRIMARY KEY, b INT);
Query OK, 0 rows affected (0.06 sec)
[InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=428, thread_id=7, space_id=76, old_file_path=./test/t1.ibd]
[InnoDB] DDL log delete : by id 428
[InnoDB] DDL log insert : [DDL record: REMOVE CACHE, id=429, thread_id=7, table_id=1102, new_file_path=test/t1]
[InnoDB] DDL log delete : by id 429
[InnoDB] DDL log insert : [DDL record: FREE, id=430, thread_id=7, space_id=76, index_id=190, page_no=4]
[InnoDB] DDL log delete : by id 430
[InnoDB] DDL log post ddl : begin for thread id : 7
InnoDB] DDL log post ddl : end for thread id : 7
從日志來看有三類操作肛炮,實(shí)際上描述了如果操作失敗需要進(jìn)行的三項逆向操作:刪除數(shù)據(jù)文件止吐,釋放內(nèi)存中的數(shù)據(jù)詞典信息宝踪,刪除索引btree。在創(chuàng)建表之前碍扔,這些數(shù)據(jù)被寫入到ddl_log中瘩燥,在創(chuàng)建完表并commit后,再從ddl log中刪除這些記錄不同。
另外上述日志中還有DDL log delete
日志厉膀,其實(shí)在每次寫入ddl log時是單獨(dú)事務(wù)提交的,但在提交之后二拐,會使用當(dāng)前事務(wù)執(zhí)行一條delete操作服鹅,直到操作結(jié)束了才會提交。
加列(instant)
mysql> ALTER TABLE t1 ADD COLUMN c INT;
Query OK, 0 rows affected (0.08 sec)
Records: 0 Duplicates: 0 Warnings: 0
[InnoDB] DDL log post ddl : begin for thread id : 7
[InnoDB] DDL log post ddl : end for thread id : 7
注意這里執(zhí)行的是Instant ddl, 這是8.0.13新支持的特性百新,加列操作可以只修改元數(shù)據(jù)企软,因此從ddl log中無需記錄數(shù)據(jù)
刪列
mysql> ALTER TABLE t1 DROP COLUMN c;
Query OK, 0 rows affected (2.77 sec)
Records: 0 Duplicates: 0 Warnings: 0
[InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=487, thread_id=7, space_id=83, old_file_path=./test/#sql-ib1108-1917598001.ibd]
[InnoDB] DDL log delete : by id 487
[InnoDB] DDL log insert : [DDL record: REMOVE CACHE, id=488, thread_id=7, table_id=1109, new_file_path=test/#sql-ib1108-1917598001]
[InnoDB] DDL log delete : by id 488
[InnoDB] DDL log insert : [DDL record: FREE, id=489, thread_id=7, space_id=83, index_id=200, page_no=4]
[InnoDB] DDL log delete : by id 489
[InnoDB] DDL log insert : [DDL record: DROP, id=490, thread_id=7, table_id=1108]
[InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=491, thread_id=7, space_id=82, old_file_path=./test/#sql-ib1109-1917598002.ibd, new_file_path=./test/t1.ibd]
[InnoDB] DDL log delete : by id 491
[InnoDB] DDL log insert : [DDL record: RENAME TABLE, id=492, thread_id=7, table_id=1108, old_file_path=test/#sql-ib1109-1917598002, new_file_path=test/t1]
[InnoDB] DDL log delete : by id 492
[InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=493, thread_id=7, space_id=83, old_file_path=./test/t1.ibd, new_file_path=./test/#sql-ib1108-1917598001.ibd]
[InnoDB] DDL log delete : by id 493
[InnoDB] DDL log insert : [DDL record: RENAME TABLE, id=494, thread_id=7, table_id=1109, old_file_path=test/t1, new_file_path=test/#sql-ib1108-1917598001]
[InnoDB] DDL log delete : by id 494
[InnoDB] DDL log insert : [DDL record: DROP, id=495, thread_id=7, table_id=1108]
[InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=496, thread_id=7, space_id=82, old_file_path=./test/#sql-ib1109-1917598002.ibd]
[InnoDB] DDL log post ddl : begin for thread id : 7
[InnoDB] DDL log replay : [DDL record: DELETE SPACE, id=496, thread_id=7, space_id=82, old_file_path=./test/#sql-ib1109-1917598002.ibd]
[InnoDB] DDL log replay : [DDL record: DROP, id=495, thread_id=7, table_id=1108]
[InnoDB] DDL log replay : [DDL record: DROP, id=490, thread_id=7, table_id=1108]
[InnoDB] DDL log post ddl : end for thread id : 7
這是個典型的三階段ddl的過程:分為prepare, perform 以及commit三個階段:
Prepare: 這個階段會修改元數(shù)據(jù),創(chuàng)建臨時ibd文件#sql-ib1108-1917598001.ibd, 如果發(fā)生異常崩潰饭望,我們需要能把這個臨時文件刪除掉仗哨, 因此和create table類似,也為這個idb寫了三條日志:delete space, remove cache,以及free btree
Perform: 執(zhí)行操作铅辞,將數(shù)據(jù)拷貝到上述ibd文件中厌漂,(同時處理online dmllog), 這部分不涉及l(fā)og ddl操作
-
Commit: 更新數(shù)據(jù)詞典信息并提交事務(wù), 這里會寫幾條日志:
DROP : table_id=1108
RENAME SPACE: #sql-ib1109-1917598002.ibd文件被rename成t1.ibd
RENAME TABLE: #sql-ib1109-1917598002被rename成t1
RENAME SPACE: t1.ibd 被rename成#sql-ib1108-1917598001.ibd
RENAME TABLE: t1表被rename成#sql-ib1108-1917598001
DROP TABLE: table_id=1108
DELETE SPACE: 刪除#sql-ib1109-1917598002.ibd
實(shí)際上這一步寫的ddl log描述了commit階段操作的逆向過程:將t1.ibd rename成#sql-ib1109-1917598002, 并將sql-ib1108-1917598001 rename成t1表,最后刪除舊表斟珊。其中刪除舊表的操作這里不執(zhí)行桩卵,而是到post-ddl階段執(zhí)行
-
Post-ddl: 在事務(wù)提交后,執(zhí)行最后的操作:replay ddl log, 刪除舊文件倍宾,清理mysql.innodb_dynamic_metadata中相關(guān)信息
DELETE SPACE:
sql-ib1109-1917598002.ibd
DROP: table_id=1108
DROP: table_id=1108
加索引
mysql> ALTER TABLE t1 ADD KEY(b);
Query OK, 0 rows affected (0.14 sec)
Records: 0 Duplicates: 0 Warnings: 0
[InnoDB] DDL log insert : [DDL record: FREE, id=431, thread_id=7, space_id=76, index_id=191, page_no=5]
[InnoDB] DDL log delete : by id 431
[InnoDB] DDL log post ddl : begin for thread id : 7
[InnoDB] DDL log post ddl : end for thread id : 7
創(chuàng)建索引采用inplace創(chuàng)建的方式,沒有臨時文件胜嗓,但如果異常發(fā)生的話高职,依然需要在發(fā)生異常時清理臨時索引, 因此增加了一條FREE log,用于異常發(fā)生時能夠刪除臨時索引辞州。
TRUNCATE TABLE
mysql> TRUNCATE TABLE t1;
Query OK, 0 rows affected (0.13 sec)
[InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=439, thread_id=7, space_id=77, old_file_path=./test/#sql-ib1103-1917597994.ibd, new_file_path=./test/t1.ibd]
[InnoDB] DDL log delete : by id 439
[InnoDB] DDL log insert : [DDL record: DROP, id=440, thread_id=7, table_id=1103]
[InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=441, thread_id=7, space_id=77, old_file_path=./test/#sql-ib1103-1917597994.ibd]
[InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=442, thread_id=7, space_id=78, old_file_path=./test/t1.ibd]
[InnoDB] DDL log delete : by id 442
[InnoDB] DDL log insert : [DDL record: REMOVE CACHE, id=443, thread_id=7, table_id=1104, new_file_path=test/t1]
[InnoDB] DDL log delete : by id 443
[InnoDB] DDL log insert : [DDL record: FREE, id=444, thread_id=7, space_id=78, index_id=194, page_no=4]
[InnoDB] DDL log delete : by id 444
[InnoDB] DDL log insert : [DDL record: FREE, id=445, thread_id=7, space_id=78, index_id=195, page_no=5]
[InnoDB] DDL log delete : by id 445
[InnoDB] DDL log post ddl : begin for thread id : 7
[InnoDB] DDL log replay : [DDL record: DELETE SPACE, id=441, thread_id=7, space_id=77, old_file_path=./test/#sql-ib1103-1917597994.ibd]
[InnoDB] DDL log replay : [DDL record: DROP, id=440, thread_id=7, table_id=1103]
[InnoDB] DDL log post ddl : end for thread id : 7
Truncate table是個比較有意思的話題怔锌,在早期5.6及之前的版本中, 是通過刪除舊表創(chuàng)建新表的方式來進(jìn)行的,5.7之后為了保證原子性变过,改成了原地truncate文件埃元,同時增加了一個truncate log文件,如果在truncate過程中崩潰媚狰,可以通過這個文件在崩潰恢復(fù)時重新truncate岛杀。
到了8.0版本,又恢復(fù)成了刪除舊表崭孤,創(chuàng)建新表的方式类嗤,與之前不同的是糊肠,8.0版本在崩潰時可以回滾到舊數(shù)據(jù),而不是再次執(zhí)行遗锣。以上述為例货裹,主要包括幾個步驟:
將表t1.ibd rename成#sql-ib1103-1917597994.ibd
創(chuàng)建新文件t1.ibd
post-ddl: 將老文件#sql-ib1103-1917597994.ibd刪除
RENAME TABLE
mysql> RENAME TABLE t1 TO t2;
Query OK, 0 rows affected (0.06 sec)
DDL LOG:
[InnoDB] DDL log insert : [DDL record: RENAME SPACE, id=450, thread_id=7, space_id=78, old_file_path=./test/t2.ibd, new_file_path=./test/t1.ibd]
[InnoDB] DDL log delete : by id 450
[InnoDB] DDL log insert : [DDL record: RENAME TABLE, id=451, thread_id=7, table_id=1104, old_file_path=test/t2, new_file_path=test/t1]
[InnoDB] DDL log delete : by id 451
[InnoDB] DDL log post ddl : begin for thread id : 7
[InnoDB] DDL log post ddl : end for thread id : 7
這個就比較簡單了,只需要記錄rename space 和rename table的逆操作即可. post-ddl不需要做實(shí)際的操作
DROP TABLE
DROP TABLE t2
[InnoDB] DDL log insert : [DDL record: DROP, id=595, thread_id=7, table_id=1119]
[InnoDB] DDL log insert : [DDL record: DELETE SPACE, id=596, thread_id=7, space_id=93, old_file_path=./test/t2.ibd]
[InnoDB] DDL log post ddl : begin for thread id : 7
[InnoDB] DDL log replay : [DDL record: DELETE SPACE, id=596, thread_id=7, space_id=93, old_file_path=./test/t2.ibd]
[InnoDB] DDL log replay : [DDL record: DROP, id=595, thread_id=7, table_id=1119]
[InnoDB] DDL log post ddl : end for thread id : 7
先在ddl log中記錄下需要刪除的數(shù)據(jù)精偿,再提交后弧圆,再最后post-ddl階段執(zhí)行真正的刪除表對象和文件操作
代碼實(shí)現(xiàn)
主要實(shí)現(xiàn)代碼集中在文件storage/innobase/log/log0ddl.cc中,包含了向log_ddl表中插入記錄以及replay的邏輯笔咽。
隱藏的innodb_log_ddl表結(jié)構(gòu)如下
def->add_field(0, "id", "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT");
def->add_field(1, "thread_id", "thread_id BIGINT UNSIGNED NOT NULL");
def->add_field(2, "type", "type INT UNSIGNED NOT NULL");
def->add_field(3, "space_id", "space_id INT UNSIGNED");
def->add_field(4, "page_no", "page_no INT UNSIGNED");
def->add_field(5, "index_id", "index_id BIGINT UNSIGNED");
def->add_field(6, "table_id", "table_id BIGINT UNSIGNED");
def->add_field(7, "old_file_path",
"old_file_path VARCHAR(512) COLLATE UTF8_BIN");
def->add_field(8, "new_file_path",
"new_file_path VARCHAR(512) COLLATE UTF8_BIN");
def->add_index(0, "index_pk", "PRIMARY KEY(id)");
def->add_index(1, "index_k_thread_id", "KEY(thread_id)");
記錄類型
根據(jù)不同的操作類型搔预,可以分為如下幾類:
FREE_TREE_LOG
目的是釋放索引btree,入口函數(shù):
log_DDL::write_free_tree_log
拓轻,在創(chuàng)建索引和刪除表時會調(diào)用到斯撮。
對于drop table中涉及的刪索引操作,log ddl的插入操作放到父事務(wù)中扶叉,一起要么提交要么回滾
對于創(chuàng)建索引的case, log ddl就需要單獨(dú)提交勿锅,父事務(wù)將記錄標(biāo)記刪除,這樣后面如果ddl回滾了枣氧,也能將殘留的index刪掉溢十。
DELETE_SPACE_LOG
入口函數(shù):
Log_DDL::write_delete_space_log
用于記錄刪除tablespace操作,同樣分為兩種情況:
drop table/tablespace, 寫入的記錄隨父事務(wù)一起提交达吞,并在post-ddl階段replay
創(chuàng)建tablespace, 寫入的記錄單獨(dú)提交张弛,并被父事務(wù)標(biāo)記刪除,如果父事務(wù)回滾酪劫,就通過replay刪除參與的tablespace
RENAME_SPACE_LOG
入口函數(shù):
Log_DDL::write_rename_space_log
用于記錄rename操作吞鸭,例如如果我們把表t1 rename成t2,在其中就記錄了逆向操作t2 rename to t1。
在函數(shù)Fil_shard::space_rename()
中覆糟,總是先寫ddl log, 再做真正的rename操作. 寫日志的過程同樣是獨(dú)立事務(wù)提交刻剥,父事務(wù)做未提交的刪除操作
DROP_LOG
入口函數(shù): Log_DDL::write_drop_log
用于記錄刪除表對象操作,這里不涉及文件層操作滩字,寫ddl log在父事務(wù)中執(zhí)行
RENAME_TABLE_LOG
入口函數(shù):
Log_DDL::write_rename_table_log
用于記錄rename table對象的逆操作造虏,和rename space類似,也是獨(dú)立事務(wù)提交ddl log, 父事務(wù)標(biāo)記刪除
REMOVE_CACHE_LOG
入口函數(shù):
Log_DDL::write_remove_cache_log
用于處理內(nèi)存表對象的清理麦箍,獨(dú)立事務(wù)提交漓藕,父事務(wù)標(biāo)記刪除
ALTER_ENCRYPT_TABLESPACE_LOG
入口函數(shù):
Log_DDL::write_alter_encrypt_space_log
用于記錄對tablespace加密屬性的修改,獨(dú)立事務(wù)提交. 在寫完ddl log后修改tablespace page0 中的加密標(biāo)記
綜上挟裂,在ddl的過程中可能會提交多次事務(wù)享钞,大概分為三類:
獨(dú)立事務(wù)寫ddl log并提交,父事務(wù)標(biāo)記刪除, 如果父事務(wù)提交了诀蓉,ddl log也被順便刪除了嫩与,如果父事務(wù)回滾了寝姿,那就要根據(jù)ddl log做逆操作來回滾ddl
獨(dú)立事務(wù)寫ddl log 并提交, (目前只有ALTER_ENCRYPT_TABLESPACE_LOG)
使用父事務(wù)寫ddl log,在ddl結(jié)束時提交划滋。需要在post-ddl階段處理
post_ddl
如上所述饵筑,有些ddl log是隨著父事務(wù)一起提交的,有些則在post-ddl階段再執(zhí)行, post_ddl發(fā)生在父事提交或回滾之后: 若事務(wù)回滾处坪,根據(jù)ddl log做逆操作根资,若事務(wù)提交,在post-ddl階段做最后真正不可逆操作(例如刪除文件)
入口函數(shù): Log_DDL::post_ddl -->Log_DDL::replay_by_thread_id
根據(jù)執(zhí)行ddl的線程thread id通過innodb_log_ddl表上的二級索引同窘,找到log id,再到聚集索引上找到其對應(yīng)的記錄項玄帕,然后再replay這些操作,完成ddl后想邦,清理對應(yīng)記錄
崩潰恢復(fù)
在崩潰恢復(fù)結(jié)束后裤纹,會調(diào)用ha_post_recover
接口函數(shù),進(jìn)而調(diào)用innodb內(nèi)的函數(shù)Log_DDL::recover()
, 同樣的replay其中的記錄丧没,并在結(jié)束后刪除記錄鹰椒。但ALTER_ENCRYPT_TABLESPACE_LOG類型并不是在這一步刪除,而是加入到一個數(shù)組ts_encrypt_ddl_records中,在之后調(diào)用resume_alter_encrypt_tablespace
來恢復(fù)操作呕童。
參考文檔
-
官方文檔:
-
WL#9536: InnoDB_New_DD: Support crash-safe DDL
https://dev.mysql.com/worklog/task/?spm=a2c4e.11153940.blogcont684418.13.7b5b4116dYdg9Y&id=9536
點(diǎn)關(guān)注漆际,不迷路
好了各位,以上就是這篇文章的全部內(nèi)容了夺饲,能看到這里的人呀奸汇,都是人才。之前說過往声,PHP方面的技術(shù)點(diǎn)很多擂找,也是因?yàn)樘嗔耍瑢?shí)在是寫不過來浩销,寫過來了大家也不會看的太多贯涎,所以我這里把它整理成了PDF和文檔,如果有需要的可以
更多學(xué)習(xí)內(nèi)容可以訪問【對標(biāo)大廠】精品PHP架構(gòu)師教程目錄大全撼嗓,只要你能看完保證薪資上升一個臺階(持續(xù)更新)
以上內(nèi)容希望幫助到大家,很多PHPer在進(jìn)階的時候總會遇到一些問題和瓶頸欢唾,業(yè)務(wù)代碼寫多了沒有方向感且警,不知道該從那里入手去提升,對此我整理了一些資料礁遣,包括但不限于:分布式架構(gòu)斑芜、高可擴(kuò)展、高性能祟霍、高并發(fā)杏头、服務(wù)器性能調(diào)優(yōu)盈包、TP6,laravel醇王,YII2呢燥,Redis,Swoole寓娩、Swoft叛氨、Kafka、Mysql優(yōu)化棘伴、shell腳本寞埠、Docker、微服務(wù)焊夸、Nginx等多個知識點(diǎn)高級進(jìn)階干貨需要的可以免費(fèi)分享給大家仁连,需要的可以加入我的PHP技術(shù)交流群953224940