在后端面試中,mysql是比不可少的一環,其中對事務和日志的考察更是"重災區", 大部分同學可能都知道mysql通過redolog、binlog和undolog保證了sql的事務性,也可以用於數據庫的數據恢復,但再深入一點,如何保證事務性?更新時數據具體是如何寫到磁盤的?這兩個日志內容不一致怎么辦?寫日志也要將日志寫到磁盤中,為什么會比直接寫數據到磁盤效率更高?..., 這些如果一問三不知,面試官(尤其大廠面試)也差不多讓你回去等消息了。
redo log與binlog
雖然可能大部分文章都有介紹過,但為了文章的完整性,我們還是從redo log和binlog的區別聊起。
位置不同
首先就是兩個日志所處的位置不同了,mysql的整體架構可分為server層和存儲引擎層,mysql采用插拔式的存儲引擎,常見的存儲引擎有myisam、innodb、memory等,在創建表時指定要使用的存儲引擎(create table .... engine=innodb)。
binlog是存在於server層的日志,也就是無論使用哪種存儲引擎,都能使用binlog記錄執行語句。而redolog是innodb存儲引擎特有的。
大小不同
binlog分多個日志文件記錄,單個文件的大小通過max_binlog_size
設置,采用追加的方式寫入,當binlog 大小超過max_binlog_size
設置的大小會創建新的日志文件,然后切換到新文件繼續寫入。此外,可通過expire_logs_days
設置binlog日志保留的天數。
redolog 的大小是固定的,在 mysql 中可以通過修改配置參數 innodb_log_files_in_group
和 innodb_log_file_size
配置日志文件數量和每個日志文件大小,采用循環寫的方式記錄,當寫到結尾時,會回到開頭循環寫日志。
記錄內容不同
binlog記錄操作的方法是邏輯性的語句。有statement和row兩種記錄格式, statement格式記 sql 語句;row 格式會記錄更新前和更新后行的內容.
而redolog記錄的是數據庫中每個頁的修改。比如“在某個數據頁上做了什么修改”
二階段更新流程
了解了兩種日志的區別后,我們再來通過一條更新語句的執行流程來看看這兩個日志分別如何寫入的。語句內容為update t set a = a + 1 where id = 1
-
執行器通過innodb引擎獲取id = 1的數據,如果數據本身在內存中,會直接返回給執行器;否則,先從磁盤中讀入內存,再返回。
-
執行器拿到引擎給的行數據,把這個值加上 1,得到新的一行數據,再調用引擎接口寫入這行新數據。
-
引擎將這行新數據更新到內存中。然后將對內存數據頁的更新內容記錄在 redolog buffer中,此時,buffer中的這條語句狀態為prepare。然后告知執行器執行完成了,隨時可以提交事務。
-
server層提交事務時,會先將這個操作的日志寫入binlog buffer中,再調用引擎的事務提交接口,引擎會將剛寫入的redolog記錄狀態修改為commit。更新完成。
可以發現,一次更新后,不僅數據存在內存中,redolog和binlog也是先寫到內存中,之后再根據設定的落盤機制進行日志落盤。
日志落盤
binlog落盤策略
mysql通過sync_binlog
參數來控制binlog buffer的日志落盤策略。
sync_binlog = 0
, 表示mysql不控制binlog的刷新,使用文件系統的緩存刷新策略,這時性能最好,同時風險也是最大的,一旦系統crash,binlog buffer中的日志數據都將丟失。
sync_binlog = 1
表示每次提交事務都會將buffer中的日志數據同步刷到磁盤中,最安全但由於刷盤頻率較高,性能也是最差的。
sync_binlog > 1
表示binlog buffer每寫入sync_binlog
次事務后,再刷日志數據到磁盤中。
redolog落盤策略
在講redolog持久化之前,我們先了解下write和fsync兩個系統調用,操作系統中,內存被划分為用戶空間和內核空間,用戶空間存放着應用程序的緩存數據,redolog buffer就存在於用戶空間中,要把用戶空間的數據持久化到磁盤中,需要先調用write系統調用,把數據先寫入內核空間,之后再調用fsync系統調用,將內核空間的數據寫入到磁盤中。
mysql通過innodb_flush_log_at_trx_commit
參數控制redo log buffer寫入磁盤的時機。
innodb_flush_log_at_trx_commit = 0
表示事務提交時,日志繼續保存在redolog buffer中,根據innodb_flush_log_at_timeout
設置的間隔調用write和fsync將日志持久化到磁盤中,innodb_flush_log_at_timeout
默認為1,也就是日志每秒寫入到磁盤中。批量寫入,io性能較好,但數據丟失風險較大。
innodb_flush_log_at_trx_commit = 1
表示事務提交時,都將調用write和fsync將日志寫入磁盤。這種方式不會丟失任何數據,但io性能較差。
innodb_flush_log_at_trx_commit = 2
表示事務提交時,都會調用write將日志寫入到內核緩存中,之后每秒調用fsync將日志寫入磁盤。這種也比較安全,即使mysql程序奔潰了,os buffer中的日志也不會丟失。當然,如果操作系統也奔潰了,這部分日志也就不見了。
Q&A
Q: 處於prepare狀態的redolog會被刷新到磁盤中嗎?
A: 會的,例如同一時刻,有a和b兩個事務,a處於prepare,b進行commit觸發日志刷盤,這時會把a的redo日志也刷到磁盤中。
Q: binlog是否是多余的,可以使用redolog代替binlog嗎?
A: 首先,就支持事務方面,binlog確實用處是不大的,在奔潰恢復的時候需要通過binlog確定事務是否該提交也只是避免binlog被應用到備庫上了,如果主庫直接回滾會導致主備數據不一致。
但binlog的”歸檔“功能是redolog不具備的。redolog大小固定,采用循環寫,較早的日志會被覆蓋,無法持久保存,而binlog是不限制大小的,日志追加寫入。只要有保留binlog日志,可以恢復數據庫任何時刻的狀態。
Q: binlog和redolog的幾種落盤策略,也是頻繁寫磁盤,與直接數據寫磁盤有什么區別嗎?
A: 日志文件是存儲在連續的若干個數據頁中的,所以在寫日志到磁盤時只需要進行一次尋址,屬於順序讀寫;而寫數據時,一次事務可能需要改動的數據可能涉及好幾個離散的數據頁,寫磁盤時需要進行多次「尋道->旋轉」的尋址過程,屬於隨機讀寫,速度比順序讀寫差了好幾個數量級。
數據落盤
為了避免頻繁寫入磁盤導致的性能瓶頸,數據頁先在內存中修改,在內存中發生過修改的頁稱為臟頁(因為此時頁中的數據與磁盤的不一致,是”臟“的), 改動的數據頁需要找時間同步到磁盤中,這個過程稱為”刷臟頁”。
LSN
在innodb中,每對一個數據頁的修改,都會生成一個8字節的序列號lsn來標記版本,lsn的值全局單調遞增,隨着日志的寫入而逐漸增大,lsn存在於數據頁和redo log中。
在整個更新過程中,有幾個lsn比較值得關注:
-
修改內存數據頁中的數據時,會更新內存數據頁中的LSN,暫稱為data_in_buffer_lsn。
-
向redolog buffer寫入日志時,會記錄下對應的LSN,暫稱為redo_log_in_buffer_lsn。
-
當觸發到redolog的幾種刷盤策略時,會將redolog buffer中的日志刷入磁盤中,並在該文件記下對應的LSN,暫稱為redo_log_on_disk_lsn。
-
數據從內存中刷到磁盤時,會在磁盤上對應的數據頁記錄下當前的LSN,暫稱為data_on_disk_lsn。
-
innodb會在適當的時候將redolog上記錄的對應數據頁的改動同步到磁盤中,同步進度也是通過lsn標示,稱為checkpoint_lsn。(后文會詳細介紹)
可以通過show engine innodb status
查看各lsn的值。
lsn可以理解為數據庫從創建以來產生的 redo 日志量,這個值越大,說明數據庫的更新越多,也可以理解為更新的時刻。此外,每個數據頁上也有一個 lsn,表示最后被修改時的 lsn,值越大表示越晚被修改。比如,數據頁 A 的 lsn 為 100,數據頁 B 的 lsn 為 200,checkpoint lsn 為 150,系統 lsn 為 300,表示當前系統已經更新到 300,小於 150 的數據頁已經被刷到磁盤上,因此數據頁 A 的最新數據一定在磁盤上,而數據頁 B 則不一定,有可能還在內存中。
下面我們來討論下innodb中發生刷臟頁的幾種時機。
數據落盤時機
定時刷新
innodb的主線程會定時將一定比例的臟頁刷新到磁盤中,這個過程是異步的,不會影響到查詢/更新等其他操作。
系統內存不夠用
innodb會維護一個內存數據頁的lru列表,並通過一個單獨的page clear線程來保證一定的空閑數據頁,當空閑頁不足時,會將lru尾部的內存頁淘汰掉,如果淘汰的頁中有臟頁,會先將臟頁數據刷新到磁盤中。
臟頁比例過高
innodb中,有個innodb_max_dirty_pages_pct
參數,用於控制臟頁在內存中的占比,當臟頁比例超過設置的比例后,會刷新一部分臟頁到磁盤中。
mysql> show variables like 'innodb_max_dirty_pages_pct';
+----------------------------+-----------+
| Variable_name | Value |
+----------------------------+-----------+
| innodb_max_dirty_pages_pct | 90.000000 |
+----------------------------+-----------+
數據庫正常關閉
參數innodb_fast_shutdown
控制着數據庫關閉時的落盤策略,當設置為1時,會將所有的日志臟頁和數據臟頁都刷新到磁盤中;設置為2時,僅保證日志落盤。
redo log checkpoint刷盤
再回顧下更新的流程,更新操作記錄到redolog,數據更新到內存中,整個更新操作就算結束了。 如果數據庫異常關閉了,下次啟動時,我們需要根據redolog將相應的數據頁的數據改動恢復回來。
但redolog大小是固定的,采用循環寫的模式,寫到結尾時,會回到開頭循環寫日志。 所以,隨着更新操作次數的積累,redolog上的記錄會被覆蓋掉,有些改動也就丟失了。
那不限制redolog的大小可以嗎?
可以試想下,redolog達到1TG,數據庫數據量有10TG,異常重啟時,為了恢復數據頁的改動。我們需要讀取1T的日志進行恢復。如果全部的數據頁都發生了修改,我們還需要將10TG的數據全部載入到內存中。 所以,不限制redolog大小后,會出現另外兩個問題:
- 恢復速度較慢;
- 內存無法緩存數據庫所有的數據。
redolog 采用checkpoint策略,會定期將redolog上的數據修改逐漸刷新到磁盤中,同步進度用lsn標示,稱為checkpoint_lsn。redolog根據checkpoint_lsn可以划分為兩部分,小於checkpoint_lsn的日志對應的數據頁改動已經刷新到磁盤中,這部分日志可被覆蓋重新寫入;大於checkpoint_lsn部分日志對應改動還未同步到磁盤。
redolog checkpoint刷盤分為異步刷盤和同步刷盤。
checkpoint_age = redo_lsn - checkpoint_lsn
async_water_mark = 75% * total_redo_log_file_size
sync_water_mark = 90% * total_redo_log_file_size
checkpoint_age < async_water_mark, 表示當前臟頁數據較少,不會觸發redolog checkpoint刷盤。
async_water_mark < checkpoint_age < sync_water_mark, 會異步將一定量的臟頁刷新到磁盤中,使得滿足checkpoint_age < async_water_mark。異步刷新不會影響其他更新操作。
checkpoint_age > sync_water_mark, 當redolog容量設置的較小,同時進行大量的更新操作,導致剩余可使用的日志較少,會觸發同步刷新,將臟頁刷新到磁盤中,直到滿足checkpoint_age < async_water_mark,同步刷新會阻塞用戶的更新操作。
Q&A
Q: 除了redolog checkpoint,其他幾種情況刷臟頁會推動checkpoint_lsn嗎?
A: 不會。緩沖池會維護一個管理臟頁的flush_list, 一個數據頁因修改了數據成為臟頁后,會添加到flush_list中,臟頁在刷新到磁盤中后,會從flush_list中去掉。
flush_list按數據頁的最早修改 lsn (oldest_modifcation) 從小到大排序。比如一個干凈頁變為臟頁后,data_in_buffer_lsn=100,在flush_list的位置為1,當數據頁再次發生改動時,data_in_buffer_lsn變為120,但在flush_list的位置不變。
進行redo checkpoint時,選擇的日志只需要與flush_list上最老的頁(擁有flsuh_list上最小的lsn)進行比較即可:
- page_noflush_list != page_noredo,表示該臟頁數據已被同步到磁盤中,推進checkpoint_lsn。
- page_noflush_list == page_noredo,將該臟頁刷新到磁盤中,推進checkpoint_lsn。
Q: checkpoint信息存在哪?如何存儲?
A: checkpoint信息存儲在第一個redo日志文件的文件頭中。儲存采用雙份存儲,輪流讀寫的方式。
在第一個redo日志文件的文件頭中有兩個地方用於存儲checkpoint信息,記錄時來回讀取這兩個checkpoint域。假設只有一個 checkpoint 域,當更新checkpoint一半時,服務器也掛了,會導致整個 checkpoint 域不可用。這樣數據庫將無法做崩潰恢復,從而無法啟動。如果有兩個 checkpoint 域,那么即使一個寫壞了,還可以用另外一個嘗試恢復,雖然有可能這個時候日志已經被覆蓋,但是至少提高了恢復成功的概率。兩個 checkpoint 域輪流寫,也能減少磁盤扇區故障帶來的影響。
奔潰恢復
用戶修改數據並成功提交了事務,此時數據改動在內存中還未落盤,如果這個時候數據庫掛了,重啟后,需要從日志中將成功提交的事務數據改動恢復后重新寫入磁盤,保證數據不丟失,同時還要回滾沒有提交的事務。奔潰恢復中,除了需要redolog和binlog日志,還離不開undo日志的支持。
undo log
進行更新操作時,都會產生undo日志:當 delete 一條記錄時,會記錄一條對應的 insert 日志。當 update 一條記錄時,會記錄一條對應相反的 update 日志.當insert一條記錄時,會記錄一條delete日志。
需要回滾事務時,只需要執行對應的undo操作,就可以將數據恢復。此外,通過undo日志,可以保證事務的隔離性。假設隔離級別設為讀提交,當未提交的事務A修改了id=1對應的行數據,此時事務B想要讀取id=1的數據,可以先拿着最新版本的數據,順着undo日志找到滿足其可見性的記錄。
undo日志與普通的數據頁一樣,對於undo頁的修改,需要先寫redo日志。也可能會由於lru的規則被淘汰出內存,之后再從磁盤中讀取。
奔潰恢復流程
整個奔潰恢復流程可以分為redo前滾
和undo回滾
兩部分。
redo前滾
對於checkpoint_lsn之前的日志,對應改動已經落盤,不需要關心。首先初始化一個hash_table,掃描checkpoint_lsn之后的日志,將同一個數據頁的日志分發到hash_table的相同位置,並按日志的lsn從小到大排序。掃描后,遍歷整個哈希表,依次應用每個數據頁的日志。應用完后,內存中數據頁的狀態就恢復到了奔潰之前。
undo回滾
接着,初始化undo日志,按操作類型分為undo_insert_list 和 undo_update_list,遍歷兩個鏈表,根據日志中記錄的事務的狀態重建事務狀態,TRX_ACTIVE表示需要回滾,TRX_STATE_PREPARED表示可能需要回滾。然后將事務加入到trx_list鏈表中,之后,遍歷trx_list,按照事務的不同狀態回滾或提交。對於TRX_ACTIVE狀態的事務,利用undo日志直接回滾;對於TRX_STATE_PREPARED狀態的事務,根據server層的binlog來決定是否回滾,如果binlog已經寫了並且日志是完整的,則提交該事務,否則就回滾。
Q&A
Q: undo日志什么時候會刪除?
A: undo按操作類型可分為update/delete/insert, insert操作在事務提交前只對當前事務可見,產生的 Undo 日志可以在事務提交后直接刪除。而update/delete操作,其他事務可能需要老版本數據,需要保留到undo操作對應的事務id比數據庫當前所有的事務快照都小(此時數據庫所有事務對此次改動均可見),才可以刪除。
寫在最后
喜歡本文的朋友,歡迎關注公眾號「會玩code」,專注大白話分享實用技術。
公眾號福利
回復【mysql】獲取免費測試數據庫!!
回復【pdf】獲取持續更新海量學習資料!!