深入理解InnoDB -- 事務(wù)篇

事務(wù)的定義

事務(wù)的基本要素(ACID)
原子性:Atomicity,整個數(shù)據(jù)庫事務(wù)是不可分割的工作單位
一致性:Consistency,事務(wù)將數(shù)據(jù)庫從一種狀態(tài)轉(zhuǎn)變?yōu)橄乱环N一致的狀態(tài)
隔離性:Isolation,每個讀寫事務(wù)的對象對其他事務(wù)的操作對象能相互分離
持久性:Durability,事務(wù)一旦提交,其結(jié)果是永久性的

事務(wù)的并發(fā)問題
臟讀:事務(wù)A讀取了事務(wù)B更新的數(shù)據(jù),然后B回滾操作,那么A讀取到的數(shù)據(jù)是臟數(shù)據(jù)
不可重復(fù)讀:事務(wù) A 多次讀取同一數(shù)據(jù),期間事務(wù) B對數(shù)據(jù)作了更新并提交,導(dǎo)致事務(wù)A多次讀取同一數(shù)據(jù)時,結(jié)果 不一致。
幻讀:事務(wù) A 多次同一條件查詢,期間事務(wù)B刪除或插入了滿足條件的數(shù)據(jù),導(dǎo)致事務(wù)A多次讀取的結(jié)果集不一致。

SQL標(biāo)準(zhǔn)定義的隔離級別為:

事務(wù)隔離級別 描述 臟讀 不可重復(fù)讀 幻讀
READ UNCOMMITTED 一個事務(wù)會讀到另一個未提交事務(wù)修改過的數(shù)據(jù)。
READ COMMITTED 一個事務(wù)只能讀到另一個已經(jīng)提交的事務(wù)修改過的數(shù)據(jù)。
REPEATABLE READ 一個事務(wù)只能讀到另一個已經(jīng)提交的事務(wù)修改過的數(shù)據(jù),而且該事務(wù)第一次讀過某條記錄后,即使其他事務(wù)修改了該記錄的值并且提交,該事務(wù)之后再讀該條記錄時,讀到的仍是第一次讀到的值。
SERIALIZABLE 事務(wù)串行化執(zhí)行

InnoDB默認(rèn)支持REPEATABLE READ,但與標(biāo)準(zhǔn)SQL不同,InnoDB在REPEATABLE READ事務(wù)隔離級別下,使用Next-Key Lock算法,避免了幻讀問題。

事務(wù)的實現(xiàn)

redo log

redo log稱為重做日志,保證事務(wù)的原子性,持久性

當(dāng)前事務(wù)數(shù)據(jù)庫系統(tǒng)普遍采用Write Ahred Log策略(WAL,預(yù)寫式日志),即當(dāng)事務(wù)提交時,先寫重做日志,再修改磁盤頁數(shù)據(jù)。如果宕機,則通過重做日志來完成數(shù)據(jù)的恢復(fù)。這也是事務(wù)ACID中D(Durability持久性)的要求。
實際上是最先操作是修改內(nèi)存池中的頁數(shù)據(jù)。先寫重做日志是相對于修改磁盤數(shù)據(jù)而言。

任何對Innodb表的更新,Innodb都會將更新操作轉(zhuǎn)化為redo log并寫入磁盤,redo log中記錄了修改的詳細(xì)信息。

redo log是物理日志,記錄頁的偏移量和字節(jié)等數(shù)據(jù)。
重做redo log,實際是重做提交事務(wù)修改的頁物理操作。

innodb_flush_log_at_trx_commit參數(shù)控制重做日志緩存刷新到磁盤的策略。
1:默認(rèn),事務(wù)提交必須調(diào)用一次fsync操作
0:事務(wù)提交時不進行寫入重做日志操作,由master thread每1秒進行一次fsync操作
2:事務(wù)提交時僅將重做日志寫入文件系統(tǒng)緩存,不進行fsync操作,當(dāng)mysql宕機而操作系統(tǒng)不宕機時,不會導(dǎo)致事務(wù)丟失。

除了事務(wù)提交時,刷新重做日志到磁盤還有如下場景

  1. redo log buffer已經(jīng)使用一半內(nèi)存空間
  2. Async/Sync Flush Checkpoint
log block

重做日志緩沖,重做日志文件都是以塊(block)的方法進行保存的,稱為重做日志塊(redo log block),每塊的大小為512字節(jié)。重做日志塊與磁盤扇區(qū)大小一樣,因此重做日志的寫入可以保證原子性,不需要doublewrite技術(shù)。

日志塊由三部分組成,依次為日志塊頭(log block header),日志內(nèi)容(log body),日志塊尾log block tailer

日志塊頭內(nèi)容以下

變量 字節(jié) 描述
LOG_BLOCK_HDR_NO 4 log buffer由log block組成,在內(nèi)部log buffer就好似一個由log block數(shù)組,LOG_BLOCK_HDR_NO用于標(biāo)記這個數(shù)組的下標(biāo),遞增并且循環(huán)使用。第一位用做flush bit,最大值為2G
LOG_BLOCK_HDR_DATA_LEN 2 表示log block占用的大小。最大為0x200,表示log block512字節(jié)
LOG_BLOCK_FIRST_REC_GROUP 2 新事務(wù)第一個日志偏移量。如果log block中存儲了事務(wù)L1后還有空余空間,還會存儲下一個事務(wù)L2的內(nèi)容,LOG_BLOCK_FIRST_REC_GROUP記錄事務(wù)L2的偏移量。

若LOG_BLOCK_FIRST_REC_GROUP與LOG_BLOCK_HDR_DATA_LEN相同,表示當(dāng)前l(fā)og block不含新的日志
LOG_BLOCK_HDR_NO是通過lsn計算得到的,因此InnoDB也可以通過lsn定位到具體的redo log。

日志塊尾只有LOG_BLOCK_TRL_NO,4字節(jié),與LOG_BLOCK_CHECKPOINT_NO保持一致

log group

重做日志組是一個邏輯概念,由多個重做日志文件redo log file組成,每個log group中的日志大小是相同的。默認(rèn)重做日志組由ib_logfile0,ib_logfile1組成。
InnoDB 1.2 版本前,重做日志組的總大小要小于4GB,InnoDB 1.2 版本開始,提高到512GB

對log block的寫入追加在redo log file的最后部分,當(dāng)一個redo log file被寫滿時,會接著寫入下一個redo log file,使用方式為round-robin。
架構(gòu)篇說過,當(dāng)頁被Checkpoint刷新到磁盤后,對應(yīng)的重做日志就不需要了 ,其空間可以被覆蓋重用。

每個redo group的第一個redo log file中前2kb不保存log block的信息,而是保存log file header,checkpoint信息。
redo log使用順序?qū)?,速度很快,但checkpoint后,需要更新第一個日志文件的頭部checkpoint標(biāo)記,這時并不是順序?qū)憽?br> redo group非第一個redo log file中前2KB內(nèi)容并不存儲信息,預(yù)留為空。

redo log file前2k內(nèi)容,依次存放以下數(shù)據(jù)

屬性 占用字節(jié)
LOG FILE HEADER 512
CHECKPOINT BLOCK1 512
512
CHECKPOINT BLOCK2 512

重點看一下CHECKPOINT BLOCK的內(nèi)容

屬性 占用字節(jié) 解析
LOG_CHECKPOINT_NO 8 單調(diào)遞增的值,每次checkpoint操作完成后進行自增操作
LOG_CHECKPOINT_LSN 8 checkpoint的值
LOG_CHECKPOINT_OFFSET 4 checkpoint的值對應(yīng)的在重做日志的偏移量

LOG_CHECKPOINT_LSN表示checkpoint的值,LSN小于該值的頁都已經(jīng)被寫入到磁盤。CHECKPOINT BLOCK有兩個,InnoDB會交替進行checkpoint值的更新。
這樣即使某次checkpoint block寫失敗了,那么崩潰恢復(fù)的時候從上一次記錄的checkpoint開始恢復(fù),也能正確的恢復(fù)數(shù)據(jù)庫事務(wù)。

恢復(fù)時,InnoDB需要讀取這兩個CHECKPOINT BLOCK,取其中較大的LOG_CHECKPOINT_LSN,恢復(fù)大于該值的重做日志。

LSN

LSN 即日志序列號( Log Sequence Number ),它是每個redo log的序號。

LSN存在多個對象中,代表不同含義

  1. 代表重做日志寫入總量
    LSN是單調(diào)遞增的,保存在log_sys中(InnoDB運行時會維護一個對象,負(fù)責(zé)管理 Redo Log Buffer,啟動時由log_init()函數(shù)負(fù)責(zé)初始化)
    每寫入一個redo log時,LSN就會遞增該 Redo Log 寫入的字節(jié)數(shù),例如新增一個log長度是len,則log_sys->lsn += len。

  2. 代表checkpoint最新位置,見上面的CHECKPOINT BLOCK中的LOG_CHECKPOINT_LSN。

當(dāng)InnoDB正常shutdown,在flush redo log和臟頁后,會做一次完全同步的checkpoint,并將checkpoint的LSN寫到共享表空間的FSP HEADER PAGE的FIL_PAGE_FILE_FLUSH_LSN變量中。
(由于已經(jīng)完全checkpoint,下次啟動時,lsn可以被重新賦予常量初始值LOG_START_LSN)

Mysql啟動時,會讀取共享表空間中的FIL_PAGE_FILE_FLUSH_LSN,以及CHECKPOINT BLOCK中的較大的LOG_CHECKPOINT_LSN,如果兩者相同,則說明正常關(guān)閉;否則,就需要進行故障恢復(fù)。
通過LOG_CHECKPOINT_LSN找到對應(yīng)的redo log,掃描其后的redo log執(zhí)行恢復(fù)操作即可。

  1. 代表頁最后刷新位置。每個頁的頭部都有一個FIL_PAGE_LSN,記錄該頁最后刷新時LSN的大小,可用于判斷頁是否需要進行恢復(fù)操作。

參數(shù):innodb_fast_shutdown,控制數(shù)據(jù)庫關(guān)閉操作
0:關(guān)閉時,需要完成所有full purge和merge insert buffer,并將所有臟頁刷新到磁盤
1:默認(rèn)值,只是將臟頁刷新到磁盤
2:只保證日志都寫入到日志文件,下次啟動,會進行恢復(fù)操作

參數(shù):innodb_force_recovery,控制數(shù)據(jù)庫恢復(fù)操作
默認(rèn)0,表示需要恢復(fù)時,進行所有的恢復(fù)操作。
其他配置值不一一列出。

undo

undo log保證事務(wù)的原子性, 幫助事務(wù)回滾以及MVCC功能。
undo是邏輯日志,對每行數(shù)據(jù)進行記錄,記錄的是每個操作的逆操作。
回滾操作,實際做的是先前相反的工作,對于insert,做一個delete,對于delete,做一個insert,對于update,做一個相反的update。

undo的存儲

InnoDB將undo log看做數(shù)據(jù),通過Page保存undo log。

回滾段
回滾段也是一個段對象,保存在頁(0,6)處(共享表空間第6頁),內(nèi)容如下

變量 字節(jié) 描述
TRX_RSEG_MAX_SIZE 未使用
TRX_RSEG_HISTORY_SZIE 4 HISTORY鏈表中UNDO頁的數(shù)量
TRX_RSGE_HISTORY 16 已提交的undo日志鏈表,可被purge回收
TRX_RSEG_FSEG_HEADER 10 回滾段的SEGMENT HEADER
TRX_RSEG_UNDO_SLOTS 4*1024 指向UNDO段SEGMENT HEADER所在頁的偏移量

一個UNDO段可以管理一個事務(wù),一個回滾段可以管理1024個UNDO段。
InnoDB1.1之前,只有一個回滾段,支持最大并發(fā)事務(wù)為1026。
InnoDB1.1開始,最大支持128個回滾段。
位于(0,5)的FIL_PAGE_TYPE_SYS,記錄了所有回滾段所在頁。

UNDO段
UNDO段是真正存儲undo log的地方。它實際上是一個UNDO頁鏈表。鏈表第一個UNDO頁由以下部分組成:

  • UNDO LOG PAGE HEADER
  • UNDO LOG SEGMENT HEADER
  • UNDO日志

UNDO LOG PAGE HEADER內(nèi)容如下

變量 字節(jié) 描述
TRX_UNDO_PAGE_TYPE 2 undo日志的類型,TRX_UNDO_INSERT或TRX_UNDO_UPDATE
TRX_UNOD_PAGE_STARE 2 UNDO頁最新一個事務(wù)undo日志所在位置
TRX_UNDO_PAGE_FREE 2 UNDO頁空閑的偏移量
TRX_UNDO_PAGE_NODE 12 UNDO頁的鏈表節(jié)點

關(guān)于TRX_UNDO_PAGE_NODE,可以參考存儲篇的鏈表結(jié)構(gòu)

UNDO LOG SEGMENT HEADER內(nèi)容如下

變量 字節(jié) 描述
TRX_UNDO_STATE 2 UNDO段的狀態(tài)
TRX_UNDO_LAST_LOG 2 最近一個undo log header在頁中的偏移量位置
TRX_UNDO_FSEG_HEADER 10 UNDO段的segment header
TRX_UNDO_PAGE_LIST 16 UNDO頁的鏈表頭

TRX_UNDO_STATE的有效值有TRX_UNDO_ACTIVE,TRX_UNDO_CACHEd,TRX_UNDO_TO_FREE,TRX_UNDO_TO_PURGE。

UNDO LOG SEGMENT HEADER僅保存在UNDO頁鏈表的第一個UNDO頁中,其他UNDO頁中對應(yīng)位置保留為空

undo記錄結(jié)構(gòu)
每個undo記錄由兩部分組成

  • UNDO LOG HEADER
  • UNDO LOG RECORD

undo log record有update undo log record和insert undo log record兩種類型,通常insert操作產(chǎn)生insert undo log record,其他DML操作產(chǎn)生update undo log record。
通過TRX_UNDO_PAGE_TYPE可以看出,一個UNDO段只能存儲一種類型的undo,insert undo log或update undo log。如果一個事務(wù)同時有INSERT,UPDATE操作,則需要每種類型分配單獨的UNDO段,這樣也會導(dǎo)致InnoDB支持最大并發(fā)事務(wù)數(shù)下降。

UNDO LOG HEADER內(nèi)容如下

變量 字節(jié) 描述
TRX_UNDO_TRX_ID 8 產(chǎn)生undo日志的事務(wù)id
TRX_UNDO_TRX_NO 8 標(biāo)識事務(wù)提交順序的序號
TRX_UNDO_DEL_MARKS 2 標(biāo)記本組 undo 日志中是否包含delete mark 產(chǎn)生的 undo 日志
TRX_UNDO_LOG_START 2 表示本組 undo 日志中第一條 undo 日志的在頁面中的偏移量
TRX_UNDO_DICT_OPERATION 2 是否為DDL操作
TRX_UNDO_TABLE_ID 8 若是DDL操作,操作表的id
TRX_UNDO_NEXT_LOG 2 下一個UNDO LOG HEADER位置
TRX_UNDO_PREV_LOG 2 上一個UNDO LOG HEADER位置
TRX_UNDO_HISTORY_NODE 12 HISTORY鏈表節(jié)點

由于purge可能移除一些undo log record,TRX_UNDO_LOG_START不一定等于UNDO LOG HEADER結(jié)束位置偏移量。

事務(wù)開啟時,會分配一個唯一的嚴(yán)格遞增的事務(wù)ID以及UNDO段,并設(shè)置其TRX_UNDO_STATE變量為TRX_UNDO_ACTIVE。

注意:
InnoDB將undo log看做數(shù)據(jù),UNDO頁與普通的數(shù)據(jù)頁一起管理,會依據(jù)LRU規(guī)則刷新出內(nèi)存,后續(xù)再從磁盤讀取。
同樣,對undo log的操作也需要記錄到redo log中。
如對于一個insert操作,redo log不僅要記錄insert操作,還需要記錄一個生成undo insert的操作。

進行恢復(fù)時,InnoDB會重做所有事務(wù),包括未提交的事務(wù)和回滾了的事務(wù)。然后通過Undo Log回滾那些未提交的事務(wù)。

參數(shù):
innodb_undo_directory:指定UNDO獨立表空間位置
innodb_undo_logs:設(shè)置rollback segment個數(shù),默認(rèn)為128(一個rollback segment支持1024并發(fā)),在InnoDB 1.2,該參數(shù)替換之前版本的innodb_rollback_segments
innodb_undo_tablespaces:組成undo表空間文件個數(shù)
innodb_undo_log_truncate: MySQL 自動收縮 Undo 表空間,防止磁盤占用過大,默認(rèn)開啟(Mysql5.7.5之后提供)
innodb_max_undo_log_size:超過該閥值將被自動收縮

UNDO頁復(fù)用
當(dāng)事務(wù)提交時,需要處理UNDO頁:

  • 如果當(dāng)前的undo log只占一個page,且占用的header page大小使用不足其3/4時(TRX_UNDO_PAGE_REUSE_LIMIT),則狀態(tài)設(shè)置為TRX_UNDO_CACHED,表示該UNDO頁可以復(fù)用,之后新的undo log記錄在當(dāng)前undo log的后面。
  • 如果是Insert_undo(undo類型為TRX_UNDO_INSERT),則狀態(tài)設(shè)置為TRX_UNDO_TO_FREE,該undo log可被刪除
  • 如上不滿足,則表明該undo log可能需要Purge線程去執(zhí)行清理操作,狀態(tài)設(shè)置為TRX_UNDO_TO_PURGE,將undo log加入到回滾段的TRX_RSGE_HISTORY中,由purge回收。

purge操作

purge用于最終完成delete和update操作,這樣設(shè)計是因為InnoDB支持MVCC,所以記錄不能在事務(wù)提交時立即進行處理,其他事務(wù)可能正在引用這行數(shù)據(jù)。
(delete操作將記錄的delete flag設(shè)置為1)

前面說過,回滾段TRX_RSGE_HISTORY列表,會根據(jù)事務(wù)提交的順序,將undo log鏈接起來。

執(zhí)行purge過程中,InnoDB從TRX_RSGE_HISTORY列表中找到第一個需要被清理的記錄trx1,清理后InnoDB會在trx1所在undo log頁繼續(xù)查找是否存在可以被清理的記錄,直到該UNDO頁沒有可以清理的記錄,再回到history list中查找下一個需要被清理的記錄。
由于可以重用,一個undo log可能存放了不同事務(wù)的undo log。因此purge操作需要涉及磁盤的離散讀取操作,是一個比較緩慢的過程。

MVCC原理

隱藏列
存儲篇說過,行數(shù)據(jù)中有兩個隱藏列用于實現(xiàn)MVCC
TransactionID:DB_TRX_ID,記錄操作該數(shù)據(jù)事務(wù)的事務(wù)ID
RollPointer:DB_ROLL_PTR,指向上一個版本數(shù)據(jù)在undo log 里的位置指針

事務(wù)修改行數(shù)據(jù)時,會將修改前的數(shù)據(jù)放入undo log中,并修改TransactionID為當(dāng)前事務(wù)ID,RollPointer指向上一個版本數(shù)據(jù)位置。
例如將行數(shù)據(jù)的一個字段從A -> B -> C,TransactionID,RollPointer變化如下



注意:這里通過RollPointer組織成一條 Undo Log 鏈。

快照
在RR級別下,事務(wù)在begin/start transaction之后的第一條select讀操作后, 會創(chuàng)建一個快照(read view),將當(dāng)前系統(tǒng)中活躍的其他事務(wù)記錄記錄起來。
在RC級別下,事務(wù)中每條select語句都會創(chuàng)建一個快照。

可見性判斷
設(shè)要讀取的行的最后提交事務(wù)id為 trx_id_current,
當(dāng)前事務(wù)創(chuàng)建的快照read view 中最早的事務(wù)id為up_limit_id, 最遲的事務(wù)id為low_limit_id

  1. trx_id_current < up_limit_id, 當(dāng)前事務(wù)在讀取該行記錄時, 該行記錄的最新事務(wù)ID是小于當(dāng)前系統(tǒng)所有活躍的事務(wù),所以當(dāng)前行數(shù)據(jù)可見。
  2. trx_id_current > low_limit_id, 當(dāng)前事務(wù)開啟后,該行記錄被修改并提交,數(shù)據(jù)不可見。
  3. up_limit_id <= trx_id_current <= low_limit_id,該行記錄最新事務(wù)處于活動狀態(tài),
    這時需要判斷 trx_id_current 在不在 快照的活躍事務(wù)ID列表中。
    若不在,數(shù)據(jù)可見。若在,不可見,需要查找 Undo Log 鏈得到上一個版本再進行可見性判斷。

group commit

對于InnoDB,事務(wù)提交進行兩個操作:

  1. 修改內(nèi)存中事務(wù)對應(yīng)的信息,并將日志寫入重做日志緩沖
  2. 調(diào)用fsync將重做日志緩沖寫入磁盤
    group commit,組提交,即將多個事務(wù)的重做日志緩沖通過一次fsync刷新到磁盤

開啟binlog后,為了保證存儲引擎中的事務(wù)和binlog的一致性,InnoDB使用兩階段事務(wù)。
注意:重做日志是innodb產(chǎn)生,物理格式日志,記錄對每個頁的修改,在事務(wù)進行中不斷寫入。
而binlog是mysql上層產(chǎn)生的,是一種邏輯日志,在事務(wù)提交完成時一次寫入。

兩階段事務(wù)步驟如下
Prepare 階段:SQL 已經(jīng)成功執(zhí)行并生成 redo 和 undo 的內(nèi)存日志;InnoDB 將回滾段設(shè)置為 prepare 狀態(tài);
binlog提交階段:binlog 內(nèi)存日志數(shù)據(jù)寫入文件系統(tǒng)緩存并通過fsync() 寫入磁盤;
Commit 階段:fsync() 將 binlog 文件系統(tǒng)緩存日志數(shù)據(jù)永久寫入磁盤;

恢復(fù)操作
在 prepare 階段前崩潰,該事務(wù)直接回滾;
在 binlog 已經(jīng) fsync() ,但 InnoDB 未 commit 時崩潰;恢復(fù)時,將會從 binlog 中獲取事務(wù)信息,重做該事務(wù)并提交,使 InnoDB 和 binlog 始終保持一致。

InnoDB需要保證 binlog 的寫入順序和 InnoDB 事務(wù)提交順序一致。



我們使用on-line backup下來的備份文件進行恢復(fù)或者主備同步,因為InnoDB檢測最新的事務(wù)T3已經(jīng)Commit,不需要進行恢復(fù),結(jié)果導(dǎo)致事務(wù)T1數(shù)據(jù)丟失。

InnoDB1.2版本前 ,使用 prepare_commit_mutex 保證順序,只有當(dāng)上一個事務(wù) commit 后釋放鎖,下個事務(wù)才可以進行 prepare 操作。但導(dǎo)致開啟二進制日志后,group commit功能失效,性能較差。

InnoDB1.2版本后進行了優(yōu)化,
prepare 階段不變,
binlog提交階段和commit 階段拆分為三個過程,每個階段都會去維護一個隊列,第一個進入該隊列的作為 leader 線程,其他作為 follower 線程;leader 線程會收集 follower 的事務(wù),并負(fù)責(zé)做 sync,follower 線程等待 leader 通知操作完成。

  • Flush階段,將隊列中每個事務(wù)的binlog都寫入內(nèi)存
  • Sync階段,將內(nèi)存隊列中的binlog刷新到磁盤,若隊列中有多個事務(wù),僅一次fsync操作就完成日志的寫入。
    Commit階段,leader根據(jù)隊列順序調(diào)用InnoDB事務(wù)的提交,這時就可以使用group commit功能。
    由于三個階段都是根據(jù)隊列順序執(zhí)行操作,所以保證 binlog 的寫入順序和 InnoDB 事務(wù)提交順序一致。

當(dāng)有一組事物在進行Commit階段時,其他新事物可以進行Flush階段,從而使用group commit不斷生效。

參考文檔:
InnoDB undo log 漫游
InnoDB 崩潰恢復(fù)

如果您覺得本文不錯,歡迎關(guān)注我的微信公眾號,您的關(guān)注是我堅持的動力!


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容