MySQL原子性與持久性的保證(undo log, redo log與binlog)

MySQL原子性與持久性的保證(undo log, redo log與binlog)
MySQL的ACID特性
原子性(Atomicity):原子性是指一個事務是一個不可分割的工作單位,其中的操作要麼都做,要麼都不做對於銀行轉賬來所就是要麼都做,要麼都不做。
一致性(Consistency):一致性是指事務執行前後,數據處於一種合法的狀態,這種狀態是語義上的而不是語法上的。
隔離性(Isolation):隔離性是指多個事務併發執行的時候,事務內部的操作與其他事務是隔離的,併發執行的各個事務之間不能互相干擾。
持久性(Durability):持久性是指事務一旦提交,它對數據庫的改變就應該是永久性的。接下來的其他操作或故障不應該對其有任何影響。
下面我們着重介紹MySQL原子性與持久性

MySQL原子性的保證–undo log(更具體的在MVCC中)
undo log的基本概念
undo log有兩個作用,提供回滾和多個行版本控制(MVCC)。
在數據修改的時候,不僅記錄了redo log,還記錄了對應的undo,如果因爲某些原因事務失敗而回滾,可以藉助該undo進行回滾。
undo log和redo log記錄物理日誌不一樣,他是邏輯日誌。可以認爲當delete一條記錄是,undo log中記錄一條對應的insert記錄,反之亦然,當update一條記錄時,他記錄一條對應相反的update記錄。
當執行rollback時,就可以從undo log中的邏輯記錄讀取到相應的內容並進行回滾,有時候應用帶行版本控制的時候,也是用過undo log來實現:當讀取帶某一行的其他事務鎖定時,它可以從undo log中分析出改行記錄以前的數據是什麼,從而提供該行版本信息,讓用戶實現非鎖定一致性讀取。
undo log採用段的方式來記錄的,每個undo操作在記錄的時候佔用一個undo log segment。
另外,undo log也會產生redo log,因爲undo log也要實現持久性的保護。

undo log的存儲方式
innoDB存儲引擎對undo的管理採用段的方式。rollback segment稱爲回滾段,每個回滾段中有1024個undo log segment。
MySQL 5.5之後可以支持128個rollback segment,即支持128*1024個undo操作,還可以通過變量innodb_undo_logs自定義多少個rollback segment,默認值爲128。
undo log默認存放在共享表空間中。即保存數據的ibdata1中,如果開啓了innodb_file_per_table,將放在每個表的.ibd文件中。
在MySQL5.6中,undo的存放位置還可以通過變量innodb_undo_directory來自定義存放目錄,默認值爲"."表示datadir。
默認rollback segment全部寫在一個文件中,但可以通過設置變量innodb_undo_tablespaces平均分配到多個文件中。

MySQL持久性的保證–redo Log與binlog
InnoDB重要的日誌模塊:redo log
在《孔乙己》這篇文章中酒店掌櫃有一個分班,專門用來記錄客人的賒賬記錄,如果賒賬的人不多,那麼它可以把顧客和賬目寫在板上,但是如果賒賬的人多了,分班總會有記不下的時候,這個時候掌櫃一定還有一個專門記錄賒賬的賬本。如果有人要賒賬或者還賬的話,掌櫃一般有兩種做法:

直接把賬本翻出來,把這次賒的帳加上去或者扣除掉;
現在粉板上記下這次的帳,等打烊以後再把賬本翻出覈算。
第一種做法對應MySQL中的做法就是每一次更新操作都需要寫進磁盤,然後磁盤也要找到對應的那條記錄,然後在更新,整個過程IO成本,查找成本都很高
第二種粉板與賬本配合的整個過程,就是MySQL裏面經常說到的WAL技術,WAL全稱爲Write-Ahead Logging,他的關鍵點就是先寫日誌,再寫磁盤,也就是先寫粉板,等不忙的時候再寫賬本。
具體來說,當有一條記錄需要更新的時候,InnoDB引擎就會先把記錄寫道redo log裏面,並更新內存,這個時候更新就算完成了。同時InnoDB引擎會在適當的時候,將這個操作記錄在磁盤裏面,而這個更新往往是在系統比較空閒的時候做的,這就像打烊以後掌櫃做的事。
如果今天賒賬的不多,掌櫃可以等打烊以後在整理,但是如果某天賒賬的特別多,粉板寫滿了,又怎麼辦呢,這個時候掌櫃只好放下手中的活,把粉板中的一部分賒賬記錄更新到賬本中,然後把這些記錄從粉板上擦掉,爲記錄新賬騰出空間。
與此類似,InnoDB的redo log是固定大小的,比如可以配置一組4個文件,每個文件的大小爲1GB,那麼這塊粉板總共就可以記錄4GB的操作,從頭開始寫,寫到末尾就又回到開頭循環寫。

write pos是當前記錄的位置,一邊寫一邊後移,寫到3號文件末尾後就會到0號文件開頭,checkpoint就是當前要擦除的位置,也是往後推移並且循環的,查出記錄前要把記錄更新到數據文件。
write pos和checkpoint之間的數據是粉板上還空則的部分,可以用來記錄新的操作,如果write pos追上checkpoint,表示粉板滿了,這時候不能在執行新的更新,得停下來先刪除掉一些記錄。
有了redo log,InnoDB就可以保證即使數據庫發生異常重啓,之前提交的記錄都不會丟失,這個能力成爲crash-safe。

重要的日誌模塊:binlog
前面我們講過,MySQL整體來看,其實就有兩塊,一塊是Server層,負責MySQL功能層面上的事情,還有一塊是引擎層,負責存儲相關的具體事宜,上面我們聊到的粉板redo log是InnoDB特有的日誌,而Server層也有自己的日誌,成爲binlog(歸檔日誌)。
那麼我想問,爲什麼又兩份日誌呢。
因爲最開始MySQL沒有InnoDB引擎,MySQL自帶的引擎是MyISAM,但是MyISAM沒有crash-safe的能力,binlog日誌只能用於歸檔,InnoDB如果只依靠binlog是沒有crash-safe能力的,所以InnoDB使用另外一套日誌系統,redo log來實現crash-safe能力。
這兩種日誌有以下三點不同:

redo log是InnoDB特有的;binlog是MySQL的Server層實現的,所有引擎都可以使用。
redo log是物理日誌,記錄的是在某個數據頁上做了什麼修改;binlog是邏輯日誌,記錄的是這個語句的原始邏輯,比如 “給ID=2這一行的c字段加1”.
redo log是循環寫的,空間固定會用完;binlog是可以追加寫入的,在文件寫道一定大小後會切換到下一個,不會覆蓋以前的日誌。
我們來看看InnoDB引擎在執行下列MySQL語句的流程。

update T set c=c+1 where ID=2;
1


執行器先找到引擎取ID=2這一行。ID是主鍵,引擎直接用樹搜索找到這一行,如果ID=2這一行所在的數據頁本來就在內存中,就直接返回給執行器;否則,需要先從磁盤讀入內存,然後再返回。
執行器拿到引擎給的行數據,把這個值加上1,比如原來是N,現在是N+1,得到新的一行數據,再調用引擎接口寫入這行新數據。
引擎將這行新數據更新到內存中,同時將這個更新操作記錄到redo log裏面,此時redo log處於prepare狀態。然後告知執行器執行完成了,隨時可以提交事務。
執行器生成這個操作的binlog,並把binlog寫入磁盤。
執行器調用引擎提交事務接口,引擎把剛剛寫入的redo log改成提交狀態,更新完成。
我們看到,最後三部被拆成了兩個步驟:prepare和commit,這就是兩階段提交。

兩階段提交
兩階段提交是爲了讓兩份日誌之間的邏輯一致,binlog會記錄所有的邏輯操作,並且採用追加寫的形式。同時系統會定期做整庫備份。這裏的定期取決於系統的重要性,可以是一天一備,也可以是一週一備。
如果需要恢復到指定的某一秒,比如某天下午兩點發現中午十二點有一次誤刪表,需要找回數據,那麼我們可以這麼做:

首先,找到最近的一次全量備份,如果你運氣好,該備份那個就是昨天晚上的一個備份,從這個備份恢復到數據庫。
從備份的時間點開始,將備份的binlog依次取出來,重放如中午誤刪表之前的哪個時刻。
爲什麼我們需要兩階段提交?
由於redolog和binlog是兩個獨立的邏輯,如果不用兩階段提交,要麼就是先寫完redo log,再寫binlog,或者採用反過來的順序,來看下會有什麼問題。
仍然用前面的update語句來做例子,假設當前ID=2的行,字段c的值是0,在假設執行update語句過程中在寫完第一個日誌後,第二個日誌還沒有寫完期間發生了crash,那麼分爲兩種情況。

先寫redo log後寫binlog。假設在redo log寫完,binlog沒有寫完的時候,MySQL進程異常重啓。由於我們前面說過的,redo log寫完之後,系統即使崩潰,仍然能夠把數據恢復回來,所以恢復後這一行c的值是1.由於binlog沒有寫完就crash了,這時候binlog裏面就沒有記錄這個語句,因此之後備份日誌的時候,存起來的binlog裏面就沒有這條語句。如果需要使用這個binlog來恢復臨時庫的話,由於這個語句的binlog丟失,這個臨時庫就會少了這一次的更新,回覆出來的這一行c的值就是0,與原庫不同。
先寫binlog後寫redo log。如果binlog寫完之後crash,由於redo log還沒寫,崩潰恢復以後這個事務無效,所以這一行的c的值是0.但是binlog裏面已經記錄了把c從0修改爲1這個日誌,所以之後再用binlog來恢復的時候就多了一個事務出來,恢復出來的這一行的值就是1,與原庫的值不同。
在兩階段提交的不同時刻MySQL出現異常,重啓後會出現什麼情況
我們假設在redo log處於prepare階段,寫binlog之前爲時刻A,在寫binlog之後,redo log處於commit階段之前爲時刻B
如果在時刻A發生crash,由於binlog還沒有寫,redo log也還沒有提交,所以崩潰恢復的時候,這個事務會回滾。這時候,binlog還沒寫,所以也不會傳到備庫中,binlog和redo log都沒有寫入,相當於沒有執行這條命令。所以MySQL重啓後和將來使用binlog進行數據恢復的數據庫的狀態是一樣的。
如果在時刻B發生crash,這時候binlog寫完,redo log還沒有commit,那麼在崩潰恢復時MySQL會做以下判斷規則:

如果redo log裏面的事務是完整的,也就是已經有了commit標識,則直接提交;
如果redo log裏面的事務只有完整的prepare,則判斷對應的事務binlog是否存在並完整:
a. 如果是,則提交事務。
b. 否則,回顧事務。
MySQL怎麼知道binlog是完整的?
一個事務的binlog是有完整格式的:

statement格式的binlog,最後會有COMMIT;
row格式的binlog,最後會有一個XID event。
另外,在MySQL 5.6.2版本以後,還引入了binlog-checksum參數,用來驗證binlog內容和正確性,對於binlog日誌由於磁盤的原因,可能會在日誌中間出錯的情況,MySQL可以通過校驗checksum的結果來發現,所以,MySQL還是有辦法驗證事務binlog的完整性的。

redo log和binlog是怎麼關聯起來的?
他們都有一個共同的數據字段,叫XID。奔潰恢復的時候,會被順序掃描redo log:

如果碰到既有prepare,又有commit的redo log,就直接提交。
如果碰到只有prepare,沒有commit的redo log,就拿着XID去binlog找對應的事情。
處於prepare階段的redolog加上完整的binlog,重啓就能恢復,MySQL爲什麼要這麼設計?
因爲binlog一旦寫入完成之後,那麼這個binlog是完整的,如果這個時候MySQL發生崩潰,在重新啓動之後,該binlog會被從庫使用,所以主庫也要提交這個事務,採用這個策略,主庫和備庫的數據就保證了一致性。

如果這樣,爲什麼還要兩階段提交?爲什麼要先redolog寫完,在寫binlog,崩潰恢復的時候,必須兩個日誌完整纔可以,是不是一樣的邏輯?
對於InnoDB引擎來說,如果redo log提交完成了,事務就不能回滾,如果允許回滾,就有可能覆蓋掉別的事務。而如果redo log直接提交,然後binlog寫入的時候失敗,InnoDB有回滾不了,數據和binlog日誌有不一致了,兩階段提交就是爲了給所有人一個機會,等待所有人OK。

不引入兩個日誌,也就沒有兩階段提交的必要了。只用binlog來支持崩潰恢復,又能支持歸檔,不就可以了?


binlog沒有能力恢復"數據頁"
在圖中所標註的位置,也就是binlog2寫完了,但是整個事務還沒有commit的時候,MySQL發生了crash。重啓後,引擎內部事務2會回滾,然後應用binlog2可以補回來,但是,對於事務1,來說,系統認爲已經提交完成,不會再應用一次binlog1。
InnoDB引擎使用的是WAL技術,執行事務的時候,寫完內存和日誌,事務就算完成了。如果之後崩潰,要依賴與日誌來恢復數據頁。
再圖中這個位置如果發生崩潰,事務1也是可能丟失了的,而且是數據頁級的丟失,此時,binlog裏面並沒有記錄數據頁的更新細節,是補不回來的。

能不能只用redo log,不用binlog?
如果只從崩潰恢復的角度來說,是可以的,但是因爲binlog擁有歸檔和賦值功能,如果關掉binlog,很多系統都無法使用,總的來說還是生態不行。

redo log一般設置多大?
如果redo log太小,會導致文件很快就被寫滿,然後不得不強行刷redo log,很容易就會使MySQL抖,這樣WAL機制的能力就發揮不出來了,如果磁盤足夠大,那麼可以設置爲4個1GB。

正常運行中的實例,數據寫入後的最終落盤,是從redo log更新過來的還是從buffer pool更新過來的?
redo log並沒有記錄數據頁的完整數據,所以他並沒有能力自己去更新磁盤數據頁,也就不存在"數據最終落盤,是由redo log更新過去" 的情況。

如果正常運行的實例,數據頁被修改後,跟磁盤的數據頁不一致,成爲髒頁,最終數據落盤,就是把內存中的數據頁寫盤,這個過程,甚至與redo log毫無關係。
再崩潰恢復場景中,InnoDB如果判斷一個數據頁可能再崩潰恢復的時候丟失了更新,就會把他讀到內存中,然後讓redo log更新內存內容,更新之後,內存頁變爲髒頁,就回到了第一種情況的狀態。
redo log buffer 是什麼? 實現修改內存,還是先寫redo log文件?
再一個事務更新過程中,日誌是要寫多次的。例如

begin;
insert into t1 ...
insert into t2 ...
commit;
1
2
3
4
這個事務要往兩個表中插入記錄,插入數據的過程中,生成的日誌都先保存起來,但又不能再還沒commit的時候就直接寫道redo log文件裏。
所以redo log buffer 就是一塊內存,用來保存redo日誌的,也就十所,再執行第一個insert的時候,數據的內存被修改了,redo log buffer 也寫入了日誌。
但是,真正把日誌寫道redo log文件,實在執行commit語句的時候做的,單獨執行一個更新語句的時候,InnoDB會自己啓動一個事務,再語句執行完成的時候提交。過程跟上面一樣,只不過是壓縮到了一個語句裏面完成。

undo與redo如何記錄事務
假設有A,B兩個數據,值分別爲1,2,開始一個事務,事務的操作內容爲:把1修改爲3,2修改爲4,那麼實際的記錄如下:

事務開始。
記錄A=1到undo log。
修改A=3。
記錄A=3到redo log。
記錄b=2到undo log。
修改B=4。
記錄B=4到redo log。
將redo log寫入磁盤。
事務提交。
上述記錄中,2,4,5,7,8均爲新增操作,但是2,4,5,7爲緩衝到buffer區,只有8增加了IO操作,爲了保證redo log能夠有比較好的IO性能,InnoDB的redo log的設計有以下幾個特點:

儘量保持redo log存儲在一段連續的空間上。因此在系統第一次啓動時就會將日誌文件的空間完全分配。以順序追加的方式記錄Redo Log,通過順序IO來改善性能。
批量寫入日誌。日誌並不是直接寫入文件,而是先寫入redo log buffer.當需要將日誌刷新到磁盤時 (如事務提交),將許多日誌一起寫入磁盤.
併發的事務共享Redo Log的存儲空間,它們的Redo Log按語句的執行順序,依次交替的記錄在一起,以減少日誌佔用的空間。
因爲3的原因,當一個事務將redo log寫入磁盤時,也會將其他未提交的事務的日誌寫入磁盤。
redo log上只進行順序追加的操作,當一個事務需要回滾時,它的redo log記錄也不會從redo log中刪除掉。
redo log的恢復
由於未提交的事務和回滾了的事務也會記錄redo log,因此在進行恢復時,這些事務要進行特殊的處理,有兩種不同的恢復策略:

進行回覆時,只重做已經提交了的事務。
進行回覆時,重做所有事務包括未提交的事務和回滾了的事務,然後通過undo log回滾的哪些未提交的事務。
MySQL數據庫InnoDB存儲引擎使用了B策略,InnoDB存儲引擎中的恢復機制有幾個特點:

在重做redo log時,並不關心事務性,恢復時,沒有begin,也沒有commit,rollback的行爲。也不關心每個日誌是哪個事務的。儘管事務ID等事務相關的內容會記入Redo Log,這些內容只是被當作要操作的數據的一部分。
使用第二個策略就必須將undo log持久化,而且必須要在寫redo log之前將對應的undo log寫入磁盤,undo和redo的這種關聯,使得持久化變得複雜起來,爲了降低複雜度,InnoDB將undo看作數據,因此記錄undo log的操作也會記錄到redo logzhong,這樣,undo log就可以像數據一樣緩存起來,而不用在redo log之前寫入磁盤。包含undo log的redo log,看起來是這樣的:
記錄1: <trx1, Undo log insert <undo_insert …>>
記錄2: <trx1, insert …>
記錄3: <trx2, Undo log insert <undo_update …>>
記錄4: <trx2, update …>
記錄5: <trx3, Undo log insert <undo_delete …>>
記錄6: <trx3, delete …>
1
2
3
4
5
6
既然Redo沒有事務性,那豈不是會重新執行被回滾了的事務?確實是這樣。同時Innodb也會將事務回滾時的操作也記錄到redo log中。回滾操作本質上也是對數據進行修改,因此回滾時對數據的操作也會記錄到Redo Log中。
記錄1: <trx1, Undo log insert <undo_insert …>>
記錄2: <trx1, insert A…>
記錄3: <trx1, Undo log insert <undo_update …>>
記錄4: <trx1, update B…>
記錄5: <trx1, Undo log insert <undo_delete …>>
記錄6: <trx1, delete C…>
記錄7: <trx1, insert C>
記錄8: <trx1, update B to old value>
記錄9: <trx1, delete A>
————————————————
版權聲明:本文爲CSDN博主「muzi劉」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/anying5823/article/details/104675987

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章