來(lái)源:https://blog.csdn.net/demonson/article/details/104369733
innodb事務(wù)日志包括redo log和undo log。redo log是重做日志,提供前滾操作,undo log是回滾日志,提供回滾操作。
undo log不是redo log的逆向過(guò)程,其實(shí)它們都算是用來(lái)恢復(fù)的日志:
1.redo log通常是物理日志,記錄的是數(shù)據(jù)頁(yè)的物理修改,而不是某一行或某幾行修改成怎樣怎樣,它用來(lái)恢復(fù)提交后的物理數(shù)據(jù)頁(yè)(恢復(fù)數(shù)據(jù)頁(yè),且只能恢復(fù)到最后一次提交的位置)。
2.undo用來(lái)回滾行記錄到某個(gè)版本。undo log一般是邏輯日志,根據(jù)每行記錄進(jìn)行記錄。
1.redo log
1.1 redo log和二進(jìn)制日志的區(qū)別
二進(jìn)制日志相關(guān)內(nèi)容,參考:MariaDB/MySQL的二進(jìn)制日志。
redo log不是二進(jìn)制日志。雖然二進(jìn)制日志中也記錄了innodb表的很多操作,
也能實(shí)現(xiàn)重做的功能,但是它們之間有很大區(qū)別。
-
二進(jìn)制日志是在
存儲(chǔ)引擎的上層產(chǎn)生的,不管是什么存儲(chǔ)引擎,對(duì)數(shù)據(jù)庫(kù)進(jìn)行了修改都會(huì)產(chǎn)生二進(jìn)制日志。而redo log是innodb層產(chǎn)生的,只記錄該存儲(chǔ)引擎中表的修改。
并且二進(jìn)制日志先于redo log被記錄。具體的見(jiàn)后文group commit小結(jié)。
-
二進(jìn)制日志記錄操作的方法是邏輯性的語(yǔ)句。即便它是基于行格式的記錄方式,其本質(zhì)也還是邏輯的SQL設(shè)置,如該行記錄的每列的值是多少。而redo log是在物理格式上的日志,它記錄的是數(shù)據(jù)庫(kù)中每個(gè)頁(yè)的修改。
-
二進(jìn)制日志只在每次事務(wù)提交的時(shí)候一次性寫(xiě)入緩存中的日志"文件"(對(duì)于非事務(wù)表的操作,則是每次執(zhí)行語(yǔ)句成功后就直接寫(xiě)入)。而redo log在數(shù)據(jù)準(zhǔn)備修改前寫(xiě)入緩存中的redo log中,然后才對(duì)緩存中的數(shù)據(jù)執(zhí)行修改操作;而且保證在發(fā)出事務(wù)提交指令時(shí),先向緩存中的redo log寫(xiě)入日志,寫(xiě)入完成后才執(zhí)行提交動(dòng)作。
-
因?yàn)槎M(jìn)制日志只在提交的時(shí)候一次性寫(xiě)入,所以二進(jìn)制日志中的記錄方式和提交順序有關(guān),且一次提交對(duì)應(yīng)一次記錄。而redo log中是記錄的物理頁(yè)的修改,redo log文件中同一個(gè)事務(wù)可能多次記錄,最后一個(gè)提交的事務(wù)記錄會(huì)覆蓋所有未提交的事務(wù)記錄。例如事務(wù)T1,可能在redo log中記錄了 T1-1,T1-2,T1-3,T1*?共4個(gè)操作,其中 T1*?表示最后提交時(shí)的日志記錄,所以對(duì)應(yīng)的數(shù)據(jù)頁(yè)最終狀態(tài)是 T1*?對(duì)應(yīng)的操作結(jié)果。而且redo log是并發(fā)寫(xiě)入的,不同事務(wù)之間的不同版本的記錄會(huì)穿插寫(xiě)入到redo log文件中,例如可能redo log的記錄方式如下:T1-1,T1-2,T2-1,T2-2,T2*,T1-3,T1*?。
-
事務(wù)日志記錄的是物理頁(yè)的情況,它具有冪等性,因此記錄日志的方式極其簡(jiǎn)練。冪等性的意思是多次操作前后狀態(tài)是一樣的,例如新插入一行后又刪除該行,前后狀態(tài)沒(méi)有變化。而二進(jìn)制日志記錄的是所有影響數(shù)據(jù)的操作,記錄的內(nèi)容較多。例如插入一行記錄一次,刪除該行又記錄一次。
1.2 redo log的基本概念
redo log包括兩部分:一是內(nèi)存中的日志緩沖(redo log buffer),該部分日志是易失性的;二是磁盤(pán)上的重做日志文件(redo log file),該部分日志是持久的。
在概念上,innodb通過(guò)
force log at commit機(jī)制實(shí)現(xiàn)事務(wù)的持久性,即在事務(wù)提交的時(shí)候,必須先將該事務(wù)的所有事務(wù)日志寫(xiě)入到磁盤(pán)上的redo log file和undo log file中進(jìn)行持久化。
為了確保每次日志都能寫(xiě)入到事務(wù)日志文件中,在每次將log buffer中的日志寫(xiě)入日志文件的過(guò)程中都會(huì)調(diào)用一次操作系統(tǒng)的fsync操作(即fsync()系統(tǒng)調(diào)用)。因?yàn)镸ariaDB/MySQL是工作在用戶空間的,MariaDB/MySQL的log buffer處于用戶空間的內(nèi)存中。要寫(xiě)入到磁盤(pán)上的log file中(redo:ib_logfileN文件,undo:share tablespace或.ibd文件),中間還要經(jīng)過(guò)操作系統(tǒng)內(nèi)核空間的os buffer,調(diào)用fsync()的作用就是將OS buffer中的日志刷到磁盤(pán)上的log file中。
也就是說(shuō),從redo log buffer寫(xiě)日志到磁盤(pán)的redo log file中,過(guò)程如下:?
在此處需要注意一點(diǎn),一般所說(shuō)的log file并不是磁盤(pán)上的物理日志文件,而是操作系統(tǒng)緩存中的log file,官方手冊(cè)上的意思也是如此(例如:With a value of 2, the contents of the
InnoDB log buffer are written to the log file?after each transaction commit and?
the log file is flushed to disk approximately once per second)。
但說(shuō)實(shí)話,這不太好理解,既然都稱為file了,應(yīng)該已經(jīng)屬于物理文件了。所以在本文后續(xù)內(nèi)容中都以os buffer或者file system buffer來(lái)表示官方手冊(cè)中所說(shuō)的Log file,然后log file則表示磁盤(pán)上的物理日志文件,即log file on disk。
另外,之所以要經(jīng)過(guò)一層os buffer,是因?yàn)閛pen日志文件的時(shí)候,open沒(méi)有使用O_DIRECT標(biāo)志位,該標(biāo)志位意味著繞過(guò)操作系統(tǒng)層的os buffer,IO直寫(xiě)到底層存儲(chǔ)設(shè)備。不使用該標(biāo)志位意味著將日志進(jìn)行緩沖,緩沖到了一定容量,或者顯式fsync()才會(huì)將緩沖中的刷到存儲(chǔ)設(shè)備。使用該標(biāo)志位意味著每次都要發(fā)起系統(tǒng)調(diào)用。比如寫(xiě)abcde,不使用o_direct將只發(fā)起一次系統(tǒng)調(diào)用,使用o_object將發(fā)起5次系統(tǒng)調(diào)用。
MySQL支持用戶自定義在commit時(shí)如何將log buffer中的日志刷log file中。這種控制通過(guò)變量 innodb_flush_log_at_trx_commit 的值來(lái)決定。該變量有3種值:0、1、2,默認(rèn)為1。但注意,這個(gè)變量只是控制commit動(dòng)作是否刷新log buffer到磁盤(pán)。
-
當(dāng)設(shè)置為1的時(shí)候,事務(wù)每次提交都會(huì)將log buffer中的日志寫(xiě)入os buffer并調(diào)用fsync()刷到log file on disk中。這種方式即使系統(tǒng)崩潰也不會(huì)丟失任何數(shù)據(jù),但是因?yàn)槊看翁峤欢紝?xiě)入磁盤(pán),IO的性能較差。
-
當(dāng)設(shè)置為0的時(shí)候,事務(wù)提交時(shí)不會(huì)將log buffer中日志寫(xiě)入到os buffer,而是每秒寫(xiě)入os buffer并調(diào)用fsync()寫(xiě)入到log file on disk中。也就是說(shuō)設(shè)置為0時(shí)是(大約)每秒刷新寫(xiě)入到磁盤(pán)中的,當(dāng)系統(tǒng)崩潰,會(huì)丟失1秒鐘的數(shù)據(jù)。
-
當(dāng)設(shè)置為2的時(shí)候,每次提交都僅寫(xiě)入到os buffer,然后是每秒調(diào)用fsync()將os buffer中的日志寫(xiě)入到log file on disk。
注意,有一個(gè)變量 innodb_flush_log_at_timeout 的值為1秒,該變量表示的是刷日志的頻率,很多人誤以為是控制 innodb_flush_log_at_trx_commit 值為0和2時(shí)的1秒頻率
實(shí)際上并非如此。測(cè)試時(shí)將頻率設(shè)置為5和設(shè)置為1,當(dāng) innodb_flush_log_at_trx_commit 設(shè)置為0和2的時(shí)候性能基本都是不變的。關(guān)于這個(gè)頻率是控制什么的,在后面的"刷日志到磁盤(pán)的規(guī)則"中會(huì)說(shuō)。
在主從復(fù)制結(jié)構(gòu)中,要保證事務(wù)的持久性和一致性,需要對(duì)日志相關(guān)變量設(shè)置為如下:
-
如果啟用了二進(jìn)制日志,則設(shè)置sync_binlog=1,即每提交一次事務(wù)同步寫(xiě)到磁盤(pán)中。
-
總是設(shè)置innodb_flush_log_at_trx_commit=1,即每提交一次事務(wù)都寫(xiě)到磁盤(pán)中。
上述兩項(xiàng)變量的設(shè)置保證了:每次提交事務(wù)都寫(xiě)入二進(jìn)制日志和事務(wù)日志,并在提交時(shí)將它們刷新到磁盤(pán)中。
選擇刷日志的時(shí)間會(huì)嚴(yán)重影響數(shù)據(jù)修改時(shí)的性能,特別是刷到磁盤(pán)的過(guò)程。下例就測(cè)試了 innodb_flush_log_at_trx_commit 分別為0、1、2時(shí)的差距。
drop table if exists test_flush_log;
create table test_flush_log(id int,name char(50))engine=innodb;
#創(chuàng)建插入指定行數(shù)的記錄到測(cè)試表中的存儲(chǔ)過(guò)程
drop procedure if exists proc;delimiter $$create procedure proc(i int)begin declare s int default 1; declare c char(50) default repeat('a',50); while s<=i do start transaction; insert into test_flush_log values(null,c); commit; set s=s+1; end while;end$$delimiter ;
當(dāng)前環(huán)境下, innodb_flush_log_at_trx_commit 的值為1,即每次提交都刷日志到磁盤(pán)。測(cè)試此時(shí)插入10W條記錄的時(shí)間。
mysql> call proc(100000);
Query OK, 0 rows affected (15.48 sec)
再測(cè)試值為2的時(shí)候,即每次提交都刷新到os buffer,但每秒才刷入磁盤(pán)中。
mysql> set @@global.innodb_flush_log_at_trx_commit=2;
mysql>?truncate?test_flush_log;
mysql> call proc(100000);
Query OK, 0 rows affected (3.41 sec)
結(jié)果插入時(shí)間大減,只需3.41秒。
最后測(cè)試值為0的時(shí)候,即每秒才刷到os buffer和磁盤(pán)。
mysql> set @@global.innodb_flush_log_at_trx_commit=0;
mysql> truncate test_flush_log;
mysql> call proc(100000);
Query OK, 0 rows affected (2.10 sec)
最后可以發(fā)現(xiàn),其實(shí)值為2和0的時(shí)候,它們的差距并不太大,但2卻比0要安全的多。它們都是每秒從os buffer刷到磁盤(pán),它們之間的時(shí)間差體現(xiàn)在log buffer刷到os buffer上。因?yàn)閷og buffer中的日志刷新到os buffer只是內(nèi)存數(shù)據(jù)的轉(zhuǎn)移,并沒(méi)有太大的開(kāi)銷(xiāo),所以每次提交和每秒刷入差距并不大??梢詼y(cè)試插入更多的數(shù)據(jù)來(lái)比較,以下是插入100W行數(shù)據(jù)的情況。從結(jié)果可見(jiàn),值為2和0的時(shí)候差距并不大,但值為1的性能卻差太多。
盡管設(shè)置為0和2可以大幅度提升插入性能,但是在故障的時(shí)候可能會(huì)丟失1秒鐘數(shù)據(jù),這1秒鐘很可能有大量的數(shù)據(jù),從上面的測(cè)試結(jié)果看,100W條記錄也只消耗了20多秒,1秒鐘大約有4W-5W條數(shù)據(jù),盡管上述插入的數(shù)據(jù)簡(jiǎn)單,但卻說(shuō)明了數(shù)據(jù)丟失的大量性。
更好的插入數(shù)據(jù)的做法是將值設(shè)置為1,然后修改存儲(chǔ)過(guò)程,將每次循環(huán)都提交修改為只提交一次,這樣既能保證數(shù)據(jù)的一致性,也能提升性能,修改如下:
drop procedure if exists proc;
delimiter $$
create procedure proc(i int)
begin
declare s int default 1;
declare c char(50) default repeat('a',50);
start transaction;
while s<=i DO
insert into test_flush_log values(null,c);
set s=s+1;
end while;
commit;
end$$
delimiter ;
mysql> set @@global.innodb_flush_log_at_trx_commit=1;
mysql>?truncate?test_flush_log;
mysql> call proc(1000000);
Query OK, 0 rows affected (11.26 sec)
1.3 日志塊(log block)
innodb存儲(chǔ)引擎中,redo log以塊為單位進(jìn)行存儲(chǔ)的,每個(gè)塊占512字節(jié),這稱為redo log block。所以不管是log buffer中還是os buffer中以及redo log file on disk中,都是這樣以512字節(jié)的塊存儲(chǔ)的。
每個(gè)redo log block由3部分組成:
日志塊頭、日志塊尾和日志主體。其中日志塊頭占用12字節(jié),日志塊尾占用8字節(jié),所以每個(gè)redo log block的日志主體部分只有512-12-8=492字節(jié)。
因?yàn)閞edo log記錄的是數(shù)據(jù)頁(yè)的變化,當(dāng)一個(gè)數(shù)據(jù)頁(yè)產(chǎn)生的變化需要使用超過(guò)492字節(jié)()的redo log來(lái)記錄,那么就會(huì)使用多個(gè)redo log block來(lái)記錄該數(shù)據(jù)頁(yè)的變化。
-
log_block_hdr_no:(4字節(jié))該日志塊在redo log buffer中的位置ID。
-
log_block_hdr_data_len:(2字節(jié))該log block中已記錄的log大小。寫(xiě)滿該log block時(shí)為0x200,表示512字節(jié)。
-
log_block_first_rec_group:(2字節(jié))該log block中第一個(gè)log的開(kāi)始偏移位置。
-
lock_block_checkpoint_no:(4字節(jié))寫(xiě)入檢查點(diǎn)信息的位置。
關(guān)于log block塊頭的第三部分 log_block_first_rec_group ,因?yàn)橛袝r(shí)候一個(gè)數(shù)據(jù)頁(yè)產(chǎn)生的日志量超出了一個(gè)日志塊,這是需要用多個(gè)日志塊來(lái)記錄該頁(yè)的相關(guān)日志。例如,某一數(shù)據(jù)頁(yè)產(chǎn)生了552字節(jié)的日志量,那么需要占用兩個(gè)日志塊,第一個(gè)日志塊占用492字節(jié),第二個(gè)日志塊需要占用60個(gè)字節(jié),那么對(duì)于第二個(gè)日志塊來(lái)說(shuō),它的第一個(gè)log的開(kāi)始位置就是73字節(jié)(60+12)。如果該部分的值和 log_block_hdr_data_len 相等,則說(shuō)明該log block中沒(méi)有新開(kāi)始的日志塊,即表示該日志塊用來(lái)延續(xù)前一個(gè)日志塊。
日志尾只有一個(gè)部分:log_block_trl_no ,該值和塊頭的 log_block_hdr_no 相等。
上面所說(shuō)的是一個(gè)日志塊的內(nèi)容,在redo log buffer或者redo log file on disk中,由很多l(xiāng)og block組成。如下圖:
1.4 log group和redo log file
log group表示的是redo log group,一個(gè)組內(nèi)由多個(gè)大小完全相同的redo log file組成。組內(nèi)redo log file的數(shù)量由變量 innodb_log_files_group 決定,默認(rèn)值為2,即兩個(gè)redo log file。這個(gè)組是一個(gè)邏輯的概念,并沒(méi)有真正的文件來(lái)表示這是一個(gè)組,但是可以通過(guò)變量 innodb_log_group_home_dir 來(lái)定義組的目錄,redo log file都放在這個(gè)目錄下,默認(rèn)是在datadir下。
mysql> show global variables like "innodb_log%";
+-----------------------------+----------+
| Variable_name | Value |
+-----------------------------+----------+
| innodb_log_buffer_size | 8388608 |
| innodb_log_compressed_pages | ON |
| innodb_log_file_size | 50331648 |
| innodb_log_files_in_group | 2 |
| innodb_log_group_home_dir | ./ |
+-----------------------------+----------+
[root@xuexi data]# ll /mydata/data/ib*
-rw-rw---- 1 mysql mysql 79691776 Mar 30 23:12 /mydata/data/ibdata1
-rw-rw---- 1 mysql mysql 50331648 Mar 30 23:12 /mydata/data/ib_logfile0
-rw-rw---- 1 mysql mysql 50331648 Mar 30 23:12 /mydata/data/ib_logfile1
可以看到在默認(rèn)的數(shù)據(jù)目錄下,有兩個(gè)ib_logfile開(kāi)頭的文件,它們就是log group中的redo log file,而且它們的大小完全一致且等于變量 innodb_log_file_size 定義的值。第一個(gè)文件ibdata1是在沒(méi)有開(kāi)啟 innodb_file_per_table 時(shí)的共享表空間文件,對(duì)應(yīng)于開(kāi)啟 innodb_file_per_table 時(shí)的.ibd文件。
在innodb將log buffer中的redo log block刷到這些log file中時(shí),會(huì)以追加寫(xiě)入的方式循環(huán)輪訓(xùn)寫(xiě)入。即先在第一個(gè)log file(即ib_logfile0)的尾部追加寫(xiě),直到滿了之后向第二個(gè)log file(即ib_logfile1)寫(xiě)。當(dāng)?shù)诙€(gè)log file滿了會(huì)清空一部分第一個(gè)log file繼續(xù)寫(xiě)入。
由于是將log buffer中的日志刷到log file,所以在log file中記錄日志的方式也是log block的方式。
在每個(gè)組的第一個(gè)redo log file中,前2KB記錄4個(gè)特定的部分,從2KB之后才開(kāi)始記錄log block。除了第一個(gè)redo log file中會(huì)記錄,log group中的其他log file不會(huì)記錄這2KB,但是卻會(huì)騰出這2KB的空間。如下:
redo log file的大小對(duì)innodb的性能影響非常大,設(shè)置的太大,恢復(fù)的時(shí)候就會(huì)時(shí)間較長(zhǎng),設(shè)置的太小,就會(huì)導(dǎo)致在寫(xiě)redo log的時(shí)候循環(huán)切換redo log file。
1.5 redo log的格式
因?yàn)閕nnodb存儲(chǔ)引擎存儲(chǔ)數(shù)據(jù)的單元是頁(yè)(和SQL Server中一樣),所以redo log也是基于頁(yè)的格式來(lái)記錄的。默認(rèn)情況下,innodb的頁(yè)大小是16KB(由 innodb_page_size 變量控制),一個(gè)頁(yè)內(nèi)可以存放非常多的log block(每個(gè)512字節(jié)),而log block中記錄的又是數(shù)據(jù)頁(yè)的變化。
其中l(wèi)og block中492字節(jié)的部分是log body,該log body的格式分為4部分:
-
redo_log_type:占用1個(gè)字節(jié),表示redo log的日志類型。
-
space:表示表空間的ID,采用壓縮的方式后,占用的空間可能小于4字節(jié)。
-
page_no:表示頁(yè)的偏移量,同樣是壓縮過(guò)的。
-
redo_log_body表示每個(gè)重做日志的數(shù)據(jù)部分,恢復(fù)時(shí)會(huì)調(diào)用相應(yīng)的函數(shù)進(jìn)行解析。例如insert語(yǔ)句和delete語(yǔ)句寫(xiě)入redo log的內(nèi)容是不一樣的。
如下圖,分別是insert和delete大致的記錄方式。
1.6 日志刷盤(pán)的規(guī)則
log buffer中未刷到磁盤(pán)的日志稱為臟日志(dirty log)。
在上面的說(shuō)過(guò),默認(rèn)情況下事務(wù)每次提交的時(shí)候都會(huì)刷事務(wù)日志到磁盤(pán)中,這是因?yàn)樽兞?innodb_flush_log_at_trx_commit 的值為1。但是innodb不僅僅只會(huì)在有commit動(dòng)作后才會(huì)刷日志到磁盤(pán),這只是innodb存儲(chǔ)引擎刷日志的規(guī)則之一。
1.發(fā)出commit動(dòng)作時(shí)。已經(jīng)說(shuō)明過(guò),commit發(fā)出后是否刷日志由變量 innodb_flush_log_at_trx_commit 控制。
2.每秒刷一次。這個(gè)刷日志的頻率由變量 innodb_flush_log_at_timeout 值決定,默認(rèn)是1秒。要注意,這個(gè)刷日志頻率和commit動(dòng)作無(wú)關(guān)。
3.當(dāng)log buffer中已經(jīng)使用的內(nèi)存超過(guò)一半時(shí)。
4.當(dāng)有checkpoint時(shí),checkpoint在一定程度上代表了刷到磁盤(pán)時(shí)日志所處的LSN位置。
1.7 數(shù)據(jù)頁(yè)刷盤(pán)的規(guī)則及checkpoint
內(nèi)存中(buffer pool)未刷到磁盤(pán)的數(shù)據(jù)稱為臟數(shù)據(jù)(dirty data)。由于數(shù)據(jù)和日志都以頁(yè)的形式存在,所以臟頁(yè)表示臟數(shù)據(jù)和臟日志。
上一節(jié)介紹了日志是何時(shí)刷到磁盤(pán)的,不僅僅是日志需要刷盤(pán),臟數(shù)據(jù)頁(yè)也一樣需要刷盤(pán)。
在innodb中,數(shù)據(jù)刷盤(pán)的規(guī)則只有一個(gè):checkpoint。但是觸發(fā)checkpoint的情況卻有幾種。
不管怎樣,checkpoint觸發(fā)后,會(huì)將buffer中臟數(shù)據(jù)頁(yè)和臟日志頁(yè)都刷到磁盤(pán)。
innodb存儲(chǔ)引擎中checkpoint分為兩種:
-
sharp checkpoint:在重用redo log文件(例如切換日志文件)的時(shí)候,將所有已記錄到redo log中對(duì)應(yīng)的臟數(shù)據(jù)刷到磁盤(pán)。
-
fuzzy checkpoint:一次只刷一小部分的日志到磁盤(pán),而非將所有臟日志刷盤(pán)。有以下幾種情況會(huì)觸發(fā)該檢查點(diǎn):
-
master thread checkpoint:由master線程控制,
每秒或每10秒刷入一定比例的臟頁(yè)到磁盤(pán)。
-
flush_lru_list checkpoint:從MySQL5.6開(kāi)始可通過(guò) innodb_page_cleaners 變量指定專門(mén)負(fù)責(zé)臟頁(yè)刷盤(pán)的page cleaner線程的個(gè)數(shù),該線程的目的是為了保證lru列表有可用的空閑頁(yè)。
-
async/sync flush checkpoint:同步刷盤(pán)還是異步刷盤(pán)。例如還有非常多的臟頁(yè)沒(méi)刷到磁盤(pán)(非常多是多少,有比例控制),這時(shí)候會(huì)選擇同步刷到磁盤(pán),但這很少出現(xiàn);如果臟頁(yè)不是很多,可以選擇異步刷到磁盤(pán),如果臟頁(yè)很少,可以暫時(shí)不刷臟頁(yè)到磁盤(pán)
-
dirty page too much checkpoint:臟頁(yè)太多時(shí)強(qiáng)制觸發(fā)檢查點(diǎn),目的是為了保證緩存有足夠的空閑空間。too much的比例由變量 innodb_max_dirty_pages_pct 控制,MySQL 5.6默認(rèn)的值為75,即當(dāng)臟頁(yè)占緩沖池的百分之75后,就強(qiáng)制刷一部分臟頁(yè)到磁盤(pán)。
由于刷臟頁(yè)需要一定的時(shí)間來(lái)完成,所以記錄檢查點(diǎn)的位置是在每次刷盤(pán)結(jié)束之后才在redo log中標(biāo)記的。
MySQL停止時(shí)是否將臟數(shù)據(jù)和臟日志刷入磁盤(pán),由變量innodb_fast_shutdown={ 0|1|2 }控制,默認(rèn)值為1,即停止時(shí)只做一部分purge,忽略大多數(shù)flush操作(但至少會(huì)刷日志),在下次啟動(dòng)的時(shí)候再flush剩余的內(nèi)容,實(shí)現(xiàn)fast shutdown。
1.8 LSN超詳細(xì)分析
LSN稱為日志的邏輯序列號(hào)(log sequence number),在innodb存儲(chǔ)引擎中,lsn占用8個(gè)字節(jié)。LSN的值會(huì)隨著日志的寫(xiě)入而逐漸增大。
根據(jù)LSN,可以獲取到幾個(gè)有用的信息:
2.寫(xiě)入的日志總量,通過(guò)LSN開(kāi)始號(hào)碼和結(jié)束號(hào)碼可以計(jì)算出寫(xiě)入的日志量。
LSN不僅存在于redo log中,還存在于數(shù)據(jù)頁(yè)中,在每個(gè)數(shù)據(jù)頁(yè)的頭部,有一個(gè)
fil_page_lsn記錄了當(dāng)前頁(yè)最終的LSN值是多少。通過(guò)數(shù)據(jù)頁(yè)中的LSN值和redo log中的LSN值比較,如果頁(yè)中的LSN值小于redo log中LSN值,則表示數(shù)據(jù)丟失了一部分,這時(shí)候可以通過(guò)redo log的記錄來(lái)恢復(fù)到redo log中記錄的LSN值時(shí)的狀態(tài)。
redo log的lsn信息可以通過(guò) show engine innodb status 來(lái)查看。MySQL 5.5版本的show結(jié)果中只有3條記錄,沒(méi)有pages flushed up to。
mysql> show engine innodb stauts
---
LOG
---
Log sequence number 2225502463
Log flushed up to 2225502463
Pages flushed up to 2225502463
Last checkpoint at 2225502463
0 pending log writes, 0 pending chkp writes
3201299 log i/o's done, 0.00 log i/o's/second
-
log sequence number就是當(dāng)前的redo log(in buffer)中的lsn;
-
log flushed up to是刷到redo log file on disk中的lsn;
-
pages flushed up to是已經(jīng)刷到磁盤(pán)數(shù)據(jù)頁(yè)上的LSN;
-
last checkpoint at是上一次檢查點(diǎn)所在位置的LSN。
innodb從執(zhí)行修改語(yǔ)句開(kāi)始:
(1).首先修改內(nèi)存中的數(shù)據(jù)頁(yè),并在數(shù)據(jù)頁(yè)中記錄LSN,暫且稱之為data_in_buffer_lsn;
(2).并且在修改數(shù)據(jù)頁(yè)的同時(shí)(幾乎是同時(shí))向redo log in buffer中寫(xiě)入redo log,并記錄下對(duì)應(yīng)的LSN,暫且稱之為redo_log_in_buffer_lsn;
(3).寫(xiě)完buffer中的日志后,當(dāng)觸發(fā)了日志刷盤(pán)的幾種規(guī)則時(shí),會(huì)向redo log file on disk刷入重做日志,并在該文件中記下對(duì)應(yīng)的LSN,暫且稱之為redo_log_on_disk_lsn;
(4).數(shù)據(jù)頁(yè)不可能永遠(yuǎn)只停留在內(nèi)存中,在某些情況下,會(huì)觸發(fā)checkpoint來(lái)將內(nèi)存中的臟頁(yè)(數(shù)據(jù)臟頁(yè)和日志臟頁(yè))刷到磁盤(pán),所以會(huì)在本次checkpoint臟頁(yè)刷盤(pán)結(jié)束時(shí),在redo log中記錄checkpoint的LSN位置,暫且稱之為checkpoint_lsn。
(5).要記錄checkpoint所在位置很快,只需簡(jiǎn)單的設(shè)置一個(gè)標(biāo)志即可,但是刷數(shù)據(jù)頁(yè)并不一定很快,例如這一次checkpoint要刷入的數(shù)據(jù)頁(yè)非常多。也就是說(shuō)要刷入所有的數(shù)據(jù)頁(yè)需要一定的時(shí)間來(lái)完成,中途刷入的每個(gè)數(shù)據(jù)頁(yè)都會(huì)記下當(dāng)前頁(yè)所在的LSN,暫且稱之為data_page_on_disk_lsn。
上圖中,從上到下的橫線分別代表:時(shí)間軸、buffer中數(shù)據(jù)頁(yè)中記錄的LSN(data_in_buffer_lsn)、磁盤(pán)中數(shù)據(jù)頁(yè)中記錄的LSN(data_page_on_disk_lsn)、buffer中重做日志記錄的LSN(redo_log_in_buffer_lsn)、磁盤(pán)中重做日志文件中記錄的LSN(redo_log_on_disk_lsn)以及檢查點(diǎn)記錄的LSN(checkpoint_lsn)。
假設(shè)在最初時(shí)(12:0:00)所有的日志頁(yè)和數(shù)據(jù)頁(yè)都完成了刷盤(pán),也記錄好了檢查點(diǎn)的LSN,這時(shí)它們的LSN都是完全一致的。
假設(shè)此時(shí)開(kāi)啟了一個(gè)事務(wù),并立刻執(zhí)行了一個(gè)update操作,執(zhí)行完成后,buffer中的數(shù)據(jù)頁(yè)和redo log都記錄好了更新后的LSN值,假設(shè)為110。這時(shí)候如果執(zhí)行 show engine innodb status 查看各LSN的值,即圖中①處的位置狀態(tài),結(jié)果會(huì)是:
log sequence number(110) > log flushed up to(100) = pages flushed up to = last checkpoint at
之后又執(zhí)行了一個(gè)delete語(yǔ)句,LSN增長(zhǎng)到150。等到12:00:01時(shí),觸發(fā)redo log刷盤(pán)的規(guī)則(其中有一個(gè)規(guī)則是 innodb_flush_log_at_timeout 控制的默認(rèn)日志刷盤(pán)頻率為1秒),這時(shí)redo log file on disk中的LSN會(huì)更新到和redo log in buffer的LSN一樣,所以都等于150,這時(shí) show engine innodb status ,即圖中②的位置,結(jié)果將會(huì)是:
log sequence number(150) = log flushed up to > pages flushed up to(100) = last checkpoint at
再之后,執(zhí)行了一個(gè)update語(yǔ)句,緩存中的LSN將增長(zhǎng)到300,即圖中③的位置。
假設(shè)隨后檢查點(diǎn)出現(xiàn),即圖中④的位置,正如前面所說(shuō),檢查點(diǎn)會(huì)觸發(fā)數(shù)據(jù)頁(yè)和日志頁(yè)刷盤(pán),但需要一定的時(shí)間來(lái)完成,所以在數(shù)據(jù)頁(yè)刷盤(pán)還未完成時(shí),檢查點(diǎn)的LSN還是上一次檢查點(diǎn)的LSN,但此時(shí)磁盤(pán)上數(shù)據(jù)頁(yè)和日志頁(yè)的LSN已經(jīng)增長(zhǎng)了,即:
log sequence number > log flushed up to 和 pages flushed up to > last checkpoint at
但是log flushed up to和pages flushed up to的大小無(wú)法確定,因?yàn)槿罩舅⒈P(pán)可能快于數(shù)據(jù)刷盤(pán),也可能等于,還可能是慢于。但是checkpoint機(jī)制有保護(hù)數(shù)據(jù)刷盤(pán)速度是慢于日志刷盤(pán)的:當(dāng)數(shù)據(jù)刷盤(pán)速度超過(guò)日志刷盤(pán)時(shí),將會(huì)暫時(shí)停止數(shù)據(jù)刷盤(pán),等待日志刷盤(pán)進(jìn)度超過(guò)數(shù)據(jù)刷盤(pán)。
等到數(shù)據(jù)頁(yè)和日志頁(yè)刷盤(pán)完畢,即到了位置⑤的時(shí)候,所有的LSN都等于300。
隨著時(shí)間的推移到了12:00:02,即圖中位置⑥,又觸發(fā)了日志刷盤(pán)的規(guī)則,但此時(shí)buffer中的日志LSN和磁盤(pán)中的日志LSN是一致的,所以不執(zhí)行日志刷盤(pán),即此時(shí) show engine innodb status 時(shí)各種lsn都相等。
隨后執(zhí)行了一個(gè)insert語(yǔ)句,假設(shè)buffer中的LSN增長(zhǎng)到了800,即圖中位置⑦。此時(shí)各種LSN的大小和位置①時(shí)一樣。
隨后執(zhí)行了提交動(dòng)作,即位置⑧。默認(rèn)情況下,提交動(dòng)作會(huì)觸發(fā)日志刷盤(pán),但不會(huì)觸發(fā)數(shù)據(jù)刷盤(pán),所以 show engine innodb status 的結(jié)果是:
log sequence number = log flushed up to > pages flushed up to = last checkpoint at
最后隨著時(shí)間的推移,檢查點(diǎn)再次出現(xiàn),即圖中位置⑨。但是這次檢查點(diǎn)不會(huì)觸發(fā)日志刷盤(pán),因?yàn)槿罩镜腖SN在檢查點(diǎn)出現(xiàn)之前已經(jīng)同步了。假設(shè)這次數(shù)據(jù)刷盤(pán)速度極快,快到一瞬間內(nèi)完成而無(wú)法捕捉到狀態(tài)的變化,這時(shí) show engine innodb status 的結(jié)果將是各種LSN相等。
1.9 innodb的恢復(fù)行為
在啟動(dòng)innodb的時(shí)候,不管上次是正常關(guān)閉還是異常關(guān)閉,總是會(huì)進(jìn)行恢復(fù)操作。
因?yàn)閞edo log記錄的是數(shù)據(jù)頁(yè)的物理變化,因此恢復(fù)的時(shí)候速度比邏輯日志(如二進(jìn)制日志)要快很多。而且,innodb自身也做了一定程度的優(yōu)化,讓恢復(fù)速度變得更快。
重啟innodb時(shí),checkpoint表示已經(jīng)完整刷到磁盤(pán)上data page上的LSN,因此恢復(fù)時(shí)僅需要恢復(fù)從checkpoint開(kāi)始的日志部分。例如,當(dāng)數(shù)據(jù)庫(kù)在上一次checkpoint的LSN為10000時(shí)宕機(jī),且事務(wù)是已經(jīng)提交過(guò)的狀態(tài)。啟動(dòng)數(shù)據(jù)庫(kù)時(shí)會(huì)檢查磁盤(pán)中數(shù)據(jù)頁(yè)的LSN,如果數(shù)據(jù)頁(yè)的LSN小于日志中的LSN,則會(huì)從檢查點(diǎn)開(kāi)始恢復(fù)。
還有一種情況,在宕機(jī)前正處于checkpoint的刷盤(pán)過(guò)程,且數(shù)據(jù)頁(yè)的刷盤(pán)進(jìn)度超過(guò)了日志頁(yè)的刷盤(pán)進(jìn)度。這時(shí)候一宕機(jī),數(shù)據(jù)頁(yè)中記錄的LSN就會(huì)大于日志頁(yè)中的LSN,在重啟的恢復(fù)過(guò)程中會(huì)檢查到這一情況,這時(shí)超出日志進(jìn)度的部分將不會(huì)重做,因?yàn)檫@本身就表示已經(jīng)做過(guò)的事情,無(wú)需再重做。
另外,事務(wù)日志具有冪等性,所以多次操作得到同一結(jié)果的行為在日志中只記錄一次。而二進(jìn)制日志不具有冪等性,多次操作會(huì)全部記錄下來(lái),在恢復(fù)的時(shí)候會(huì)多次執(zhí)行二進(jìn)制日志中的記錄,速度就慢得多。例如,某記錄中id初始值為2,通過(guò)update將值設(shè)置為了3,后來(lái)又設(shè)置成了2,在事務(wù)日志中記錄的將是無(wú)變化的頁(yè),根本無(wú)需恢復(fù);而二進(jìn)制會(huì)記錄下兩次update操作,恢復(fù)時(shí)也將執(zhí)行這兩次update操作,速度比事務(wù)日志恢復(fù)更慢。
1.10 和redo log有關(guān)的幾個(gè)變量
-
innodb_flush_log_at_trx_commit={0|1|2} # 指定何時(shí)將事務(wù)日志刷到磁盤(pán),默認(rèn)為1。
-
0表示每秒將"log buffer"同步到"os buffer"且從"os buffer"刷到磁盤(pán)日志文件中。
-
1表示每事務(wù)提交都將"log buffer"同步到"os buffer"且從"os buffer"刷到磁盤(pán)日志文件中。
-
2表示每事務(wù)提交都將"log buffer"同步到"os buffer"但每秒才從"os buffer"刷到磁盤(pán)日志文件中。
-
innodb_log_buffer_size:# log buffer的大小,默認(rèn)8M
-
innodb_log_file_size:#事務(wù)日志的大小,默認(rèn)5M
-
innodb_log_files_group =2:# 事務(wù)日志組中的事務(wù)日志文件個(gè)數(shù),默認(rèn)2個(gè)
-
innodb_log_group_home_dir =./:# 事務(wù)日志組路徑,當(dāng)前目錄表示數(shù)據(jù)目錄
-
innodb_mirrored_log_groups =1:# 指定事務(wù)日志組的鏡像組個(gè)數(shù),但鏡像功能好像是強(qiáng)制關(guān)閉的,所以只有一個(gè)log group。在MySQL5.7中該變量已經(jīng)移除。
2.undo log
2.1 基本概念
undo log有兩個(gè)作用:提供回滾和多個(gè)行版本控制(MVCC)。
在數(shù)據(jù)修改的時(shí)候,不僅記錄了redo,還記錄了相對(duì)應(yīng)的undo,如果因?yàn)槟承┰驅(qū)е率聞?wù)失敗或回滾了,可以借助該undo進(jìn)行回滾。
undo log和redo log記錄物理日志不一樣,它是邏輯日志。
可以認(rèn)為當(dāng)delete一條記錄時(shí),undo log中會(huì)記錄一條對(duì)應(yīng)的insert記錄,反之亦然,當(dāng)update一條記錄時(shí),它記錄一條對(duì)應(yīng)相反的update記錄。
當(dāng)執(zhí)行rollback時(shí),就可以從undo log中的邏輯記錄讀取到相應(yīng)的內(nèi)容并進(jìn)行回滾。有時(shí)候應(yīng)用到行版本控制的時(shí)候,也是通過(guò)undo log來(lái)實(shí)現(xiàn)的:當(dāng)讀取的某一行被其他事務(wù)鎖定時(shí),它可以從undo log中分析出該行記錄以前的數(shù)據(jù)是什么,從而提供該行版本信息,讓用戶實(shí)現(xiàn)非鎖定一致性讀取。
undo log是采用段(segment)的方式來(lái)記錄的,每個(gè)undo操作在記錄的時(shí)候占用一個(gè)undo log segment。
另外,
undo log也會(huì)產(chǎn)生redo log,因?yàn)閡ndo log也要實(shí)現(xiàn)持久性保護(hù)。
2.2 undo log的存儲(chǔ)方式
innodb存儲(chǔ)引擎對(duì)undo的管理采用段的方式。
rollback segment稱為回滾段,每個(gè)回滾段中有1024個(gè)undo log segment。
在以前老版本,只支持1個(gè)rollback segment,這樣就只能記錄1024個(gè)undo log segment。后來(lái)MySQL5.5可以支持128個(gè)rollback segment,即支持128*1024個(gè)undo操作,還可以通過(guò)變量 innodb_undo_logs (5.6版本以前該變量是 innodb_rollback_segments )自定義多少個(gè)rollback segment,默認(rèn)值為128。
undo log默認(rèn)存放在共享表空間中。
[root@xuexi data]# ll /mydata/data/ib*
-rw-rw---- 1 mysql mysql 79691776 Mar 31 01:42 /mydata/data/ibdata1
-rw-rw---- 1 mysql mysql 50331648 Mar 31 01:42 /mydata/data/ib_logfile0
-rw-rw---- 1 mysql mysql 50331648 Mar 31 01:42 /mydata/data/ib_logfile1
如果開(kāi)啟了 innodb_file_per_table ,將放在每個(gè)表的.ibd文件中。
在MySQL5.6中,undo的存放位置還可以通過(guò)變量 innodb_undo_directory 來(lái)自定義存放目錄,默認(rèn)值為"."表示datadir。
默認(rèn)rollback segment全部寫(xiě)在一個(gè)文件中,但可以通過(guò)設(shè)置變量 innodb_undo_tablespaces 平均分配到多少個(gè)文件中。該變量默認(rèn)值為0,即全部寫(xiě)入一個(gè)表空間文件。該變量為靜態(tài)變量,只能在數(shù)據(jù)庫(kù)示例停止?fàn)顟B(tài)下修改,如寫(xiě)入配置文件或啟動(dòng)時(shí)帶上對(duì)應(yīng)參數(shù)。但是innodb存儲(chǔ)引擎在啟動(dòng)過(guò)程中提示,不建議修改為非0的值,如下:
2017-03-31 13:16:00 7f665bfab720 InnoDB: Expected to open 3 undo tablespaces but was able
2017-03-31 13:16:00 7f665bfab720 InnoDB: to find only 0 undo tablespaces.
2017-03-31 13:16:00 7f665bfab720 InnoDB: Set the innodb_undo_tablespaces parameter to the
2017-03-31 13:16:00 7f665bfab720 InnoDB: correct value and retry. Suggested value is 0
2.3 和undo log相關(guān)的變量
undo相關(guān)的變量在MySQL5.6中已經(jīng)變得很少。如下:它們的意義在上文中已經(jīng)解釋了。
mysql> show variables like "%undo%";
+-------------------------+-------+
| Variable_name | Value |
+-------------------------+-------+
| innodb_undo_directory | . |
| innodb_undo_logs | 128 |
| innodb_undo_tablespaces | 0 |
+-------------------------+-------+
2.4 delete/update操作的內(nèi)部機(jī)制
當(dāng)事務(wù)提交的時(shí)候,innodb不會(huì)立即刪除undo log,因?yàn)楹罄m(xù)還可能會(huì)用到undo log,如隔離級(jí)別為repeatable read時(shí),事務(wù)讀取的都是開(kāi)啟事務(wù)時(shí)的最新提交行版本,只要該事務(wù)不結(jié)束,該行版本就不能刪除,即undo log不能刪除。
但是在事務(wù)提交的時(shí)候,會(huì)將該事務(wù)對(duì)應(yīng)的undo log放入到刪除列表中,未來(lái)通過(guò)purge來(lái)刪除。并且提交事務(wù)時(shí),還會(huì)判斷undo log分配的頁(yè)是否可以重用,如果可以重用,則會(huì)分配給后面來(lái)的事務(wù),避免為每個(gè)獨(dú)立的事務(wù)分配獨(dú)立的undo log頁(yè)而浪費(fèi)存儲(chǔ)空間和性能。
通過(guò)undo log記錄delete和update操作的結(jié)果發(fā)現(xiàn):(insert操作無(wú)需分析,就是插入行而已)
-
delete操作實(shí)際上不會(huì)直接刪除,而是將delete對(duì)象打上delete flag,標(biāo)記為刪除,最終的刪除操作是purge線程完成的。
-
update分為兩種情況:update的列是否是主鍵列。
-
如果不是主鍵列,在undo log中直接反向記錄是如何update的。即update是直接進(jìn)行的。
-
如果是主鍵列,update分兩部執(zhí)行:先刪除該行,再插入一行目標(biāo)行。
3.binlog和事務(wù)日志的先后順序及group commit
如果事務(wù)不是只讀事務(wù),即涉及到了數(shù)據(jù)的修改,默認(rèn)情況下會(huì)在commit的時(shí)候調(diào)用fsync()將日志刷到磁盤(pán),保證事務(wù)的持久性。
但是一次刷一個(gè)事務(wù)的日志性能較低,特別是事務(wù)集中在某一時(shí)刻時(shí)事務(wù)量非常大的時(shí)候。innodb提供了group commit功能,可以將多個(gè)事務(wù)的事務(wù)日志通過(guò)一次fsync()刷到磁盤(pán)中。
因?yàn)槭聞?wù)在提交的時(shí)候不僅會(huì)記錄事務(wù)日志,還會(huì)記錄二進(jìn)制日志,但是它們誰(shuí)先記錄呢?二進(jìn)制日志是MySQL的上層日志,先于存儲(chǔ)引擎的事務(wù)日志被寫(xiě)入。
在MySQL5.6以前,當(dāng)事務(wù)提交(即發(fā)出commit指令)后,MySQL接收到該信號(hào)進(jìn)入commit prepare階段;進(jìn)入prepare階段后,立即寫(xiě)內(nèi)存中的二進(jìn)制日志,寫(xiě)完內(nèi)存中的二進(jìn)制日志后就相當(dāng)于確定了commit操作;然后開(kāi)始寫(xiě)內(nèi)存中的事務(wù)日志;最后將二進(jìn)制日志和事務(wù)日志刷盤(pán),它們?nèi)绾嗡⒈P(pán),分別由變量 sync_binlog 和 innodb_flush_log_at_trx_commit 控制。
但因?yàn)橐WC二進(jìn)制日志和事務(wù)日志的一致性,在提交后的prepare階段會(huì)啟用一個(gè)prepare_commit_mutex鎖來(lái)保證它們的順序性和一致性。但這樣會(huì)導(dǎo)致開(kāi)啟二進(jìn)制日志后group commmit失效,特別是在主從復(fù)制結(jié)構(gòu)中,幾乎都會(huì)開(kāi)啟二進(jìn)制日志。
在MySQL5.6中進(jìn)行了改進(jìn)。提交事務(wù)時(shí),在存儲(chǔ)引擎層的上一層結(jié)構(gòu)中會(huì)將事務(wù)按序放入一個(gè)隊(duì)列,隊(duì)列中的第一個(gè)事務(wù)稱為leader,其他事務(wù)稱為follower,leader控制著follower的行為。雖然順序還是一樣先刷二進(jìn)制,再刷事務(wù)日志,但是機(jī)制完全改變了:刪除了原來(lái)的prepare_commit_mutex行為,也能保證即使開(kāi)啟了二進(jìn)制日志,group commit也是有效的。
MySQL5.6中分為3個(gè)步驟:flush階段、sync階段、commit階段。
-
flush階段:向內(nèi)存中寫(xiě)入每個(gè)事務(wù)的二進(jìn)制日志。
-
sync階段:將內(nèi)存中的二進(jìn)制日志刷盤(pán)。若隊(duì)列中有多個(gè)事務(wù),那么僅一次fsync操作就完成了二進(jìn)制日志的刷盤(pán)操作。這在MySQL5.6中稱為BLGC(binary log group commit)。
-
commit階段:leader根據(jù)順序調(diào)用存儲(chǔ)引擎層事務(wù)的提交,由于innodb本就支持group commit,所以解決了因?yàn)殒i prepare_commit_mutex 而導(dǎo)致的group commit失效問(wèn)題。
在flush階段寫(xiě)入二進(jìn)制日志到內(nèi)存中,但是不是寫(xiě)完就進(jìn)入sync階段的,而是要等待一定的時(shí)間,多積累幾個(gè)事務(wù)的binlog一起進(jìn)入sync階段,等待時(shí)間由變量 binlog_max_flush_queue_time 決定,默認(rèn)值為0表示不等待直接進(jìn)入sync,設(shè)置該變量為一個(gè)大于0的值的好處是group中的事務(wù)多了,性能會(huì)好一些,但是這樣會(huì)導(dǎo)致事務(wù)的響應(yīng)時(shí)間變慢,所以建議不要修改該變量的值,除非事務(wù)量非常多并且不斷的在寫(xiě)入和更新。
進(jìn)入到sync階段,會(huì)將binlog從內(nèi)存中刷入到磁盤(pán),刷入的數(shù)量和單獨(dú)的二進(jìn)制日志刷盤(pán)一樣,由變量 sync_binlog 控制。
當(dāng)有一組事務(wù)在進(jìn)行commit階段時(shí),其他新事務(wù)可以進(jìn)行flush階段,它們本就不會(huì)相互阻塞,所以group commit會(huì)不斷生效。當(dāng)然,group commit的性能和隊(duì)列中的事務(wù)數(shù)量有關(guān),如果每次隊(duì)列中只有1個(gè)事務(wù),那么group commit和單獨(dú)的commit沒(méi)什么區(qū)別,當(dāng)隊(duì)列中事務(wù)越來(lái)越多時(shí),即提交事務(wù)越多越快時(shí),group commit的效果越明顯。
特別推薦一個(gè)分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒(méi)關(guān)注的小伙伴,可以長(zhǎng)按關(guān)注一下:
長(zhǎng)按訂閱更多精彩▼
如有收獲,點(diǎn)個(gè)在看,誠(chéng)摯感謝