MySQL事務之-2
在上一篇中我們提到了MySQL的事務特性,這一片主要講述事務的實現。
事務的隔離性由鎖來實現。原子性,一致性,永續性通過資料庫的redo和undo log來實現。
redo恢復提交事務修改頁的操作,而undo回滾行記錄到某個特定版本。因此兩者記錄的內容不同,redo通常是物理日誌,記錄的是頁的物理修改操作。undo是邏輯日誌,根據每行記錄進行記錄。
資料庫為了保證提交資料的永續性,做了很多努力;回想一下double write,首先把緩衝池中的髒頁使用double write技術重新整理到磁碟上;想象一種情況,在一個事務的過程中,資料庫down掉了,然後資料庫恢復的時候會發生什麼?因為最後一條事務既沒有提交也沒有回滾,這時候資料庫狀態違反了事務的原子性,因此在進行crash recover的時候,需要判斷事務是需要提交呢,還是回滾?因此我們引入了redo日誌和undo日誌。【感覺說的不太準確】
redo日誌
重做日誌由兩部分組成;一是記憶體中的重做日誌緩衝(redo_log buffer);二是重做日誌檔案,日誌檔案是持久的!
在事務進行提交時,必須首先將該事務的所有日誌寫入重做日誌檔案進行持久化,然後進行comit操作。在INNODB儲存引擎中,事務日誌由兩部分組成redo log和undo log。redo log用來保證事務的永續性,undo log用來幫助事務回滾以及MVCC功能。
redo log基本上是順序寫的,在資料庫執行時不需要對redo log進行讀取操作,而undo log是需要進行隨機讀取的。
為了確保每次日誌都寫入重做日誌檔案,在每次講重做日誌緩衝寫入重做日誌檔案後,INNODB儲存引擎都會呼叫一次fsync操作。【在重新整理的時候雖然開啟了O_DIRECT選項,但是檔案的元資訊並不會改變,因此需要呼叫fsync函式】因此重做日誌緩衝先寫入檔案系統快取,為了確保重做日誌寫入磁碟,必須進行一次fsync操作。由於fsync操作的效率取決於磁碟的效能,因此磁碟的效能決定了事務提交的效能,也就是資料庫的效能。
innodb_flush_log_at_trx_commit用來控制重做日誌重新整理到磁碟的策略。
innodb_flush_log_at_trx_commit:
取值如下:
1: 表示事務提交時必須呼叫一次fsync操作。
0: 表示事務提交時不進行寫入重做日誌操作,這個操作僅在master thread中完成,而master thread會每1秒進行一次fsync操作。
2:表示事務提交時將重做日誌寫入重做日誌檔案,但僅寫入檔案系統的快取,不進行fsync操作,在這個設定下資料庫宕機並不會導致事務丟失,但是伺服器宕機會導致事務丟失
二進位制日誌和重做日誌
在MySQL資料庫中還有一種二進位制日誌,其用來進行PIT恢復以及主從複製環境的建立。
重做日誌是從INNODB儲存引擎層產生的,而二進位制日誌是MySQL資料庫的上層產生的。【因為MySQL有不同的儲存引擎,為了保證各個引擎的相容(複製的時候),因此有了二進位制日誌】。
二進位制日誌是一種邏輯日誌,其記錄的是對應的SQL語句,而INNODB儲存引擎的重做日誌是物理格式,其記錄的是對於每個行的修改。
還有就是寫入的時間點不同,二進位制日誌是在事務提交的時候一次性寫入的,而重做日誌在事務進行中不斷的被寫入,重做日誌並不是隨事務的提交順序進行寫入的。
重做日誌的內部組成
重做日誌都是以512位元組的塊進行儲存的,稱之為重做日誌塊(redo log block)。磁碟的每個扇區的大小也是512位元組,因為重做日誌的的寫入是原子性的,不需要double write。
重做日誌塊除了日誌本身以外,還有日誌頭和日誌尾兩部分組成。重做日誌頭一共佔用12位元組,重做日誌尾佔用8位元組,因為每個重做日誌塊實際可以儲存大小為492位元組,
如圖顯示了重做日誌快取的結構,可以發現重做日誌快取由每個512位元組大小的日誌塊組成。
log block header的組成如下:
名稱 | 佔用位元組 |
LOG_BLOCK_HDR_NO | 4 |
LOG_BLOCK_HDR_DATA_LEN | 2 |
LOG_BLOCK_FIRST_REC_GROUP | 2 |
LOG_BLOCK_CHECKPOINT_NO | 4 |
log buffer是由log block組成,在內部log buffer就好似一個數組,因此LOG_BLOCK_HDR_NO用來標記這個陣列中的位置。其是遞增並且迴圈使用的,佔用4個位元組,但是由於第一位用來判斷是否是flush bit,所以最大的值為2G。(這個應該最大限制應該是MySQL5.5版本的限制,重做日誌最大為4G,MySQL5.6之後最大為512G)
LOG_BLOCK_HDR_DATA_LEN: 表示log block所佔用的大小。當log block被寫滿時,該值為0x200.
LOG_BLOCK_FIRST_REC_GROUP:表示第一個日誌所在的偏移量。當一個新log block寫入資料時,其偏移量為log block的頭部大小為12位元組。事務t1重做日誌為762位元組,事務t2的重做日誌為100位元組,那麼需要兩個log block,第一個日誌日誌塊的偏移量為12位元組,第二個日誌塊為(762-492+12)=280位元組。
LOG_BLOCK_CHECKPOINT_NO:表示該log block最後被寫入時的檢查點,佔用4位元組。
日誌尾只有一部分組成,其值和LOG_BLOCK_HDR_NO相同。
重做日誌的大小管理
在預設的情況下,在INNODB儲存引擎的資料目錄下面有兩個名為ib_logfile0和ib_logfile1的檔案。每個INNODB儲存引擎至少有1個重做日誌檔案組,每個檔案組下至少有兩個檔案。為了提高可靠性,使用者可以設定多個映象組,將不同的檔案組放在不同的磁碟上,一次提高重做日誌的高可用性。在重做日誌組中,每個重做日誌檔案的大小是一樣的,並以迴圈的方式寫入資料。
首先寫入重做日誌檔案1,達到檔案最後時,會切換到檔案2,輪轉迴圈寫入資料。
#注意: MySQL5.7中沒有映象這個設定了
mysql> show variables like "innodb_mirrored_log_groups"; Empty set (0.01 sec) mysql> set innodb_mirrored_log_groups = 2; ERROR 1193 (HY000): Unknown system variable 'innodb_mirrored_log_groups'
重做日誌引數設定如下:
innodb_log_file_size: 設定重做日誌的大小。(總的大小,在只有1個重做日誌組的時候,每個重做日誌的大小=innodb_log_file_size/innodb_log_files_in_group)
innodb_log_files_in_group: 每個重做日誌組有多少個重做日誌檔案
innodb_log_group_home_dir: 指定重做日誌的位置,預設是在datadir指定的目錄下面
innodb_log_buffer_size: log buffer的大小,預設是16
重做日誌檔案大小設定對於INNODB儲存引擎的效能有著非常大的影響。一方面重做日誌檔案不能設定的太大,如果設定太大,在恢復時可能需要很長時間;另一方面又不能設定的太小,否則可能導致一個事務日誌需要多次切換重做日誌檔案,頻繁發生async checkpoin,導致效能抖動。
在INNODB儲存引擎中,對於不同的操作有著不同的重做日誌格式。雖然各種重做日誌的型別不同,但是他們有著基本的格式,如下:
redo_log_type | space | page_no | redo_log_body |
redo_log_type: 佔用1位元組,表示重做日誌的型別。
space:表示表空間ID。
page_no:表示頁便宜量。
redo_log_body: 表示每個重做日誌的資料部分,恢復時需要呼叫相應的函式進行解析。
undo日誌
重做日誌記錄了事務的行為,可以很好地通過其對頁進行“重做”操作。但是事務有時候還要進行回滾操作,這時就需要undo。對資料庫進行修改時,INNODB儲存引擎不但會產生redo,還會產生一定量的undo。
undo存放在資料庫內部一個特殊段中,這個段稱為undo段。undo段位於共享表空間內。
undo是邏輯日誌,因此只是將資料庫邏輯地恢復到原來的樣子。所有修改都被邏輯地取消了,但是資料結構和頁本身在回滾之後可能大不相同。
除了回滾操作,undo的另一個作用是MVCC,即非鎖定一致性讀。
undo儲存管理:
INNODB儲存引擎對undo的管理採用段的方式。在INNODB儲存引擎中有rollback segment,每個回滾段記錄了1024個undo log segment。而在每個undo log segmetn段中進行undo頁的申請。共享表空間偏移量為5的頁記錄了所有rollback segment header所在的頁,這個頁型別為FILE_PAGE_TYPE_SYS。
在INNODB1.1版本之前只有一個rollback segment,因此同時支援最大線上事務數為1024.從1.1版本之後最大支援128個rollback segment,其中32個回滾段作為保留用於臨時表事務的非重做回滾段,剩餘的96個可用的回滾段,每個回滾段支援1023個線上事務的連線。
InnoDB supports 128 rollback segments, 32 of which are reserved as non-redo rollback segments for temporary table transactions. Each transaction
that updates a temporary table (excluding read-only transactions) is assigned two rollback segments, one redo-enabled rollback segment and one
non-redo rollback segment. Read-only transactions are only assigned non-redo rollback segments, as read-only transactions are only permitted to
modify temporary tables. This leaves 96 available rollback segments, each of which supports up to 1023 concurrent data-modifying transactions, for a total limit of
approximately 96K concurrent data-modifying transactions. The 96K limit assumes that transactions do not modify temporary tables. If all
data-modifying transactions also modify temporary tables, the total limit is approximately 32K concurrent data modifying transactions.
For more information about rollback segments that are reserved for temporary table transactions, see Temporary Table Undo Logs.
詳見官方文件: https://dev.mysql.com/doc/refman/5.7/en/innodb-undo-logs.html
在INNODB1.1(包含)之前回滾段都存在於共享表空間中,在INNODB1.2之後,可以通過引數對回滾段做進一步的設定。
mysql> show variables like "innodb%undo%"; +--------------------------+------------+ | Variable_name | Value | +--------------------------+------------+ | innodb_max_undo_log_size | 1073741824 | #最大回滾段的大小 | innodb_undo_directory | ./ | #回滾段的檔案位置,預設在datadir指定的目錄下面 | innodb_undo_log_truncate | OFF | # | innodb_undo_logs | 128 | #支援的回滾段大小,預設是128個 | innodb_undo_tablespaces | 0 | #構成回滾段檔案的數量,若設定,則檔名以undo開頭 +--------------------------+------------+ 5 rows in set (0)
innodb_undo_log_truncate:回收undo表空間,要求MySQL資料庫至少有兩個undo表空間,一個保持活躍的狀態,另一個處於被回收的狀態。
超過innodb_max_undo_log_size定義大小的表空間將會被回收。MySQL5.7才加入的!
mysql> show global variables like '%truncate%';
+--------------------------------------+-------+
| Variable_name | Value |
+--------------------------------------+-------+
| innodb_purge_rseg_truncate_frequency | 128 |
| innodb_undo_log_truncate | OFF |
+--------------------------------------+-------+
2 rows in set (0.01 sec)
undo的truncate主要由下面兩個引數控制:innodb_purge_rseg_truncate_frequency,innodb_undo_log_truncate。 1. innodb_undo_log_truncate是開關引數。 2. innodb_purge_rseg_truncate_frequency預設128,表示purge undo輪詢128次後,進行一次undo的truncate。 當設定innodb_undo_log_truncate=ON的時候, undo表空間的檔案大小,如果超過了innodb_max_undo_log_size, 就會被truncate到初始大小,但有一個前提,就是表空間中的undo不再被使用。 其主要步驟如下: 1. 超過大小了之後,會被mark truncation,一次會選擇一個 2. 選擇的undo不能再分配新給新的事務 3. purge執行緒清理不再需要的rollback segment 4. 等所有的回滾段都釋放了後,truncate操作,使其成為install db時的初始狀態。 預設情況下, 是purge觸發128次之後,進行一次rollback segment的free操作,然後如果全部free就進行一個truncate。 但mark的操作需要幾個依賴條件需要滿足: 1. 系統至少得有兩個undo表空間,防止一個offline後,至少另外一個還能工作 2. 除了ibdata裡的segment,還至少有兩個segment可用 3. undo表空間的大小確實超過了設定的閾值MySQL5.7--truncate說明
事務在undo log segment分配頁並寫入undo log的這個過程同樣需要寫入重做日誌。也就是undo會產生redo log。
當事務提交時,INNODB儲存引擎會做以下兩件事:
- 將undo放入到列表中,供之後的purge操作。
- 判斷undo log所在頁是否可以重用,若可以分配給下個事務使用。
事務提交後並不馬上刪除undo log及undo log所在的頁。這是因為可能還有其他事務需要通過undo log來得到行記錄之前的版本。故事務將undo log放入一個連結串列中,是否可以刪除undo log以及undo log所在的頁由purge執行緒決定。
事務提交時,將undo頁放入連結串列中,然後判斷給undo頁的使用空間是否小於3/4,若是則表明該undo頁可以重用,之後,新的undo log記錄在當前undo log的後面。由於存放undo log的列表都是以記錄進行組織的,而undo頁可能存放著不同事務的undo log,因此purge執行緒操作需要涉及磁碟的離散讀取操作,是一個比較緩慢的過程。
TRANSACTIONS ------------ Trx id counter 12041 Purge done for trx's n:o < 0 undo n:o < 0 state: running but idle History list length 0 #undo 頁的大小 LIST OF TRANSACTIONS FOR EACH SESSION: ---TRANSACTION 421780424370000, not started 0 lock struct(s),