數據庫通常借助日志來實現事務,常見的有undo log、redo log,undo/redo log都能保證事務特性,undolog實現事務原子性,redolog實現事務的持久性。
為了最大程度避免數據寫入時io瓶頸帶來的性能問題,MySQL采用了這樣一種緩存機制:當query修改數據庫內數據時,InnoDB先將該數據從磁盤讀取到內存中,修改內存中的數據拷貝,並將該修改行為持久化到磁盤上的事務日志(先寫redo log buffer,再定期批量寫入),而不是每次都直接將修改過的數據記錄到硬盤內,等事務日志持久化完成之后,內存中的臟數據可以慢慢刷回磁盤,稱之為Write-Ahead Logging。事務日志采用的是追加寫入,順序io會帶來更好的性能優勢。
為了避免臟數據刷回磁盤過程中,掉電或系統故障帶來的數據丟失問題,InnoDB采用事務日志(redo log)來解決該問題。
一、先簡單了解幾個概念
數據庫數據存放的文件稱為data file;
日志文件稱為log file;
數據庫數據是有緩存的,如果沒有緩存,每次都寫或者讀物理disk,那性能就太低下了。數據庫數據的緩存稱為data buffer,日志(redo)緩存稱為log buffer。
內存緩沖池
buffer pool如果mysql不用內存緩沖池,每次讀寫數據時,都需要訪問磁盤,必定會大大增加I/O請求,導致效率低下。所以Innodb引擎在讀寫數據時,把相應的數據和索引載入到內存中的緩沖池(buffer pool)中,一定程度的提高了數據讀寫的速度。
buffer pool:占最大塊內存,用來存放各種數據的緩存包括有索引頁、數據頁、undo頁、插入緩沖、自適應哈希索引、innodb存儲的鎖信息、數據字典信息等。工作方式總是將數據庫文件按頁(每頁16k)讀取到緩沖池,然后按最近最少使用(lru)的算法來保留在緩沖池中的緩存數據。如果數據庫文件需要修改,總是首先修改在緩存池中的頁(發生修改后即為臟頁dirty page),然后再按照一定的頻率將緩沖池的臟頁刷新到文件。
表空間
表空間可看做是InnoDB存儲引擎邏輯結構的最高層。 表空間文件:InnoDB默認的表空間文件為ibdata1。
-
段:表空間由各個段組成,常見的段有數據段、索引段、回滾段(undo log段)等。
-
區:由64個連續的頁組成,每個頁大小為16kb,即每個區大小為1MB。
-
頁:每頁16kb,且不能更改。常見的頁類型有:數據頁、Undo頁、系統頁、事務數據頁、插入緩沖位圖頁、插入緩沖空閑列表頁、未壓縮的二進制大對象頁、壓縮的二進制大對象頁。
redo log 和undo log
為了滿足事務的持久性,防止buffer pool數據丟失,innodb引入了redo log。為了滿足事務的原子性,innodb引入了undo log。
二、undo log
Undo log 是為了實現事務的原子性。還用Undo Log來實現多版本並發控制(簡稱:MVCC)。
delete/update操作的內部機制
當事務提交的時候,innodb不會立即刪除undo log,因為后續還可能會用到undo log,如隔離級別為repeatable read時,事務讀取的都是開啟事務時的最新提交行版本,只要該事務不結束,該行版本就不能刪除,即undo log不能刪除。
但是在事務提交的時候,會將該事務對應的undo log放入到刪除列表中,未來通過purge來刪除。並且提交事務時,還會判斷undo log分配的頁是否可以重用,如果可以重用,則會分配給后面來的事務,避免為每個獨立的事務分配獨立的undo log頁而浪費存儲空間和性能。
通過undo log記錄delete和update操作的結果發現:(insert操作無需分析,就是插入行而已)
- delete操作實際上不會直接刪除,而是將delete對象打上delete flag,標記為刪除,最終的刪除操作是purge線程完成的。
- update分為兩種情況:update的列是否是主鍵列。
- 如果不是主鍵列,在undo log中直接反向記錄是如何update的。即update是直接進行的。
- 如果是主鍵列,update分兩部執行:先刪除該行,再插入一行目標行。
①事務的原子性
事務的所有操作,要么全部完成,要不都不做,不能只做一半。如果在執行的過程中發生了錯誤,要回到事務開始時的狀態,所有的操作都要回滾。
②原理
Undo Log的原理很簡單,為了滿足事務的原子性,在操作任何數據之前,首先將數據備份到一個地方(這個存儲數據備份的地方稱為Undo Log)。然后進行數據的修改。如果出現了錯誤或者用戶執行了ROLLBACK語句,系統可以利用Undo Log中的備份將數據恢復到事務開始之前的狀態。
假設有A、B兩個數據,值分別為1,2。 進行+2的事務操作。
A.事務開始.
B.記錄A=1到undo log.
C.修改A=3.
D.記錄B=2到undo log.
E.修改B=4.
F.將undo log寫到磁盤。
G.將數據寫到磁盤。
H.事務提交
這里有一個隱含的前提條件:‘數據都是先讀到內存中,然后修改內存中的數據,最后將數據寫回磁盤’。
之所以能同時保證原子性和持久化,是因為以下特點:
A. 更新數據前記錄Undo log。
B. 為了保證持久性,必須將數據在事務提交前寫到磁盤。只要事務成功提交,數據必然已經持久化。
C. Undo log必須先於數據持久化到磁盤。如果在G,H之間系統崩潰,undo log是完整的,可以用來回滾事務。
D. 如果在A-F之間系統崩潰,因為數據沒有持久化到磁盤。所以磁盤上的數據還是保持在事務開始前的狀態。
缺點:每個事務提交前將數據和Undo Log寫入磁盤,這樣會導致大量的磁盤IO,因此性能很低。
三、Redo Log
redo log就是保存執行的SQL語句到一個指定的Log文件,當mysql執行數據恢復時,重新執行redo log記錄的SQL操作即可。引入buffer pool會導致更新的數據不會實時持久化到磁盤,當系統崩潰時,雖然buffer pool中的數據丟失,數據沒有持久化,但是系統可以根據Redo Log的內容,將所有數據恢復到最新的狀態。redo log在磁盤上作為一個獨立的文件存在。默認情況下會有兩個文件,名稱分別為 ib_logfile0和ib_logfile1。
參數innodb_log_file_size指定了redo log的大小;innodb_log_file_in_group指定了redo log的數量,默認為2; innodb_log_group_home_dir指定了redo log所在路徑。
innodb_additional_mem_pool_size = 100M
innodb_buffer_pool_size = 128M
innodb_data_home_dir = /home/mysql/local/mysql/var
innodb_data_file_path = ibdata1:1G:autoextend
innodb_file_io_threads = 4
innodb_thread_concurrency = 16
innodb_flush_log_at_trx_commit = 1
innodb_log_buffer_size = 8M
innodb_log_file_size = 128M
innodb_log_file_in_group = 2
innodb_log_group_home_dir = /home/mysql/local/mysql/var
為了滿足事務的原子性,在操作任何數據之前,首先將數據備份到Undo Log,然后進行數據的修改。如果出現了錯誤或者用戶執行了ROLLBACK語句,系統可以利用Undo Log中的備份將數據恢復到事務開始之前的狀態。與redo log不同的是,磁盤上不存在單獨的undo log文件,它存放在數據庫內部的一個特殊段(segment)中,這稱為undo段(undo segment),undo段位於共享表空間內。
Innodb為每行記錄都實現了三個隱藏字段:
- 6字節的事務ID(DB_TRX_ID)
- 7字節的回滾指針(DB_ROLL_PTR)
- 隱藏的ID
redo log的記錄內容
undo log和 redo log本身是分開的。innodb的undo log是記錄在數據文件(ibd)中的,而且innodb將undo log的內容看作是數據,因此對undo log本身的操作(如向undo log中插入一條undo記錄等),都會記錄redo log。undo log可以不必立即持久化到磁盤上。即便丟失了,也可以通過redo log將其恢復。因此當插入一條記錄時:
- 向undo log中插入一條undo log記錄。
- 向redo log中插入一條”插入undo log記錄“的redo log記錄。
- 插入數據。
- 向redo log中插入一條”insert”的redo log記錄。
redo log的io性能
為了保證Redo Log能夠有比較好的IO性能,InnoDB 的 Redo Log的設計有以下幾個特點:
- 盡量保持Redo Log存儲在一段連續的空間上。因此在系統第一次啟動時就會將日志文件的空間完全分配。以順序追加的方式記錄Redo Log。
- 批量寫入日志。日志並不是直接寫入文件,而是先寫入redo log buffer,然后每秒鍾將buffer中數據一並寫入磁盤
- 並發的事務共享Redo Log的存儲空間,它們的Redo Log按語句的執行順序,依次交替的記錄在一起,以減少日志占用的空間。
- Redo Log上只進行順序追加的操作,當一個事務需要回滾時,它的Redo Log記錄也不會從Redo Log中刪除掉。
①原理
和Undo Log相反,Redo Log記錄的是新數據的備份。在事務提交前,只要將Redo Log持久化即可,不需要將數據持久化。當系統崩潰時,雖然數據沒有持久化,但是Redo Log已經持久化。系統可以根據Redo Log的內容,將所有數據恢復到最新的狀態。
②Undo + Redo事務的簡化過程
A.事務開始.
B.記錄A=1到undo log.
C.修改A=3.
D.記錄A=3到redo log.
E.記錄B=2到undo log.
F.修改B=4.
G.記錄B=4到redo log.
H.將redo log寫入磁盤。
I.事務提交
四、小結
A-G的過程是在內存中進行的,相應的操作記錄在redo log buffer(B&E),redo log buffer(E&G)中,事務執行結果(此時未提交)也存在db buffer中(C&F),buffer滿了就寫入磁盤當中,如果buffer存儲的事務數量都是1個,也就意味着是將日志立即刷入磁盤,那么數據的一致性很好保證。如果存儲多個的話,是一次事務完成就會先將redo log同步到磁盤當中並有一個狀態位來記錄是否提交,再去真正的提交事務,將db buffer 中的數據同步到DB的磁盤當中去。要保證在db buffer中的內容寫入磁盤數據庫文件之前,應當把log buffer的內容寫入磁盤日志文件。這種方式可以減少磁盤IO,增加吞吐量。 不過,這種方式適用於一致性要求不高的情景。因為如果出現斷電等系統故障,log buffer、db buffer中的完成的事務還沒同步到磁盤會丟失。 像銀行這種要求事務較高的一致性,就一定要保證每次事務都要記錄到磁盤中,如果服務器down了的時候去redo log中恢復,重做一次已經提交的事務。
五、redo & undo log的作用
- 數據持久化
buffer pool中維護一個按臟頁修改先后順序排列的鏈表,叫flush_list。根據flush_list中頁的順序刷數據到持久存儲。按頁面最早一次被修改的順序排列。正常情況下,dirty page什么時候flush到磁盤上呢?
- 當redo空間占滿時,將會將部分dirty page flush到disk上,然后釋放部分redo log。
- 當需要在Buffer pool分配一個page,但是已經滿了,這時候必須 flush dirty pages to disk。一般地,可以通過啟動參數 innodb_max_dirty_pages_pct控制這種情況,當buffer pool中的dirty page到達這個比例的時候,把dirty page flush到disk中。
- 檢測到系統空閑的時候,會flush。
- 數據恢復
隨着時間的積累,Redo Log會變的很大。如果每次都從第一條記錄開始恢復,恢復的過程就會很慢,從而無法被容忍。為了減少恢復的時間,就引入了Checkpoint機制。假設在某個時間點,所有的臟頁都被刷新到了磁盤上。這個時間點之前的所有Redo Log就不需要重做了。系統記錄下這個時間點時redo log的結尾位置作為checkpoint。在進行恢復時,從這個checkpoint的位置開始即可。Checkpoint點之前的日志也就不再需要了,可以被刪除掉。
六、恢復(Recovery)
恢復策略
前面說到未提交的事務和回滾了的事務也會記錄Redo Log,因此在進行恢復時,這些事務要進行特殊的處理.有2種不同的恢復策略:
A. 進行恢復時,只重做已經提交了的事務。(返回給客戶端的是已經提交一定保證數據的可恢復持久性)
B. 進行恢復時,重做所有事務包括未提交的事務和回滾了的事務。然后通過Undo Log回滾那些未提交的事務。比如在B-E過程中down機了,那么恢復時根據undo log去重新模擬當時的情景(但是如果log buffer的空間很大,log沒有同步到磁盤這個過程就沒有辦法來進行,同時由於事務沒有提交,返回給客戶端的值是未提交成功,所以也沒有關系)