事務(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ù)提交時,刷新重做日志到磁盤還有如下場景
- redo log buffer已經(jīng)使用一半內(nèi)存空間
- 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存在多個對象中,代表不同含義
代表重做日志寫入總量
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。代表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ù)操作即可。
- 代表頁最后刷新位置。每個頁的頭部都有一個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 | 4 | 未使用 |
| 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
- trx_id_current < up_limit_id, 當(dāng)前事務(wù)在讀取該行記錄時, 該行記錄的最新事務(wù)ID是小于當(dāng)前系統(tǒng)所有活躍的事務(wù),所以當(dāng)前行數(shù)據(jù)可見。
- trx_id_current > low_limit_id, 當(dāng)前事務(wù)開啟后,該行記錄被修改并提交,數(shù)據(jù)不可見。
- 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ù)提交進行兩個操作:
- 修改內(nèi)存中事務(wù)對應(yīng)的信息,并將日志寫入重做日志緩沖
- 調(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)注是我堅持的動力!
