InnoDB存儲引擎
InnoDB存儲結構
表空間
則每張表都會有一個表空間(xxx.ibd),一個mysql實例可以對應多個表空間
- 系統表空間
- 存儲數據字典(表結構定義、索引信息等)、Change Buffer、Doublewrite Buffer
- undo log,默認在此可更改到獨立表空間
- 默認存儲在
ibdata1
文件中
- 獨立表空間
- 每個表單獨對應一個
.ibd
文件(存儲表數據和索引)
- 每個表單獨對應一個
- 通用表空間
- 存儲多個表的數據和索引
- 臨時表空間
- 臨時表數據
CREATE TEMPORARY TABLE
- 排序和聚合操作的臨時數據
ORDER BY
、GROUP BY
等 - JOIN多表連接的臨時數據
- 臨時表數據
- Undo 表空間
- 存儲 Undo Log(默認位于系統表空間,可分離)
段
- 數據段:B+樹葉子節點
- 索引段:B+樹非葉子結點
- 回滾段:管理undo log
區
- 連續分配的最小單元(1區 = 64個連續頁 = 1MB也就是)
- 作用:減少隨機 I/O(預分配連續空間),避免大量小頁零散分布
頁
- 磁盤IO最小單元(默認 16KB)
行
- InnoDB 存儲引擎數據是按行進行存放的
InnoDB的內存架構
核心組件
緩沖池Buffer Pool
- 作用:緩存磁盤數據頁,減少磁盤IO操作
- LRU算法(最近最少使用)
- 分區管理:
- 新子列表 (37%):頻繁訪問的熱數據
- 舊子列表 (63%):新加載的冷數據
- 在專用服務器上,通常將多達**80%**的物理內存分配給緩沖池
日志緩沖區Log Buffer
- 用來保存要寫入到磁盤中的log日志數據(redo log 、undo log), 默認大小為 16MB,日志緩沖區的日志會定期刷新到磁盤中。如果需要更新、插入或刪除許多行的事 務,增加日志緩沖區的大小可以減少磁盤 I/O。
- 參數:
- innodb_log_buffer_size:緩沖區大小
- innodb_flush_log_at_trx_commit:日志刷新到磁盤時機,取值主要包含以下三個:
- 1: 日志在每次事務提交時寫入并刷新到磁盤,默認值。
- 0: 每秒將日志寫入并刷新到磁盤一次。
- 2: 日志在每次事務提交后寫入,并每秒刷新到磁盤一次。
更改緩沖區Change Buffer
針對非唯一二級索引,在執行DML語句時,如果這些語句不在Buffer Pool中,不會直接操作磁盤進行修改,而是先將數據變更存在Change Buffer中,在未來數據讀取時,將數據合并到Buffer Pool中,再將合并后的數據刷新到磁盤中。
自適應哈希索引
自適應hash索引,用于優化對Buffer Pool數據的查詢。MySQL的innoDB引擎中雖然沒有直接支持 hash索引,但是給我們提供了一個功能就是這個自適應hash索引。因為前面我們講到過,hash索引在 進行等值匹配時,一般性能是要高于B+樹的,因為hash索引一般只需要一次IO即可,而B+樹,可能需 要幾次匹配,所以hash索引的效率要高,但是hash索引又不適合做范圍查詢、模糊匹配等。 InnoDB存儲引擎會監控對表上各索引頁的查詢,如果觀察到在特定的條件下hash索引可以提升速度, 則建立hash索引,稱之為自適應hash索引。
MVCC
多版本并發控制,是數據庫實現高并發訪問的核心技術,維護一個數據的多個版本,使得MySQL能在RR和RC級別不使用鎖機制的情況下實現非阻塞讀,同時保證事務的隔離性。
RU讀取最新的數據版本,除事務回滾用到undo log不涉及MVCC快照讀。
S將所有讀操作隱式轉換為當前讀(FOR SHARE),同樣不涉及快照讀。
MVCC 核心組成
組件 | 作用 |
---|---|
Undo Log | 存儲數據歷史版本鏈 |
---------------- | |
Read View | 事務開啟時生成的"數據可見性快照" |
----------------- | |
表中隱藏列 | 記錄事務版本信息 |
DB_TRX_ID | 最近修改/插入該數據的事務ID,最后一次修改該記錄的事務ID |
DB_ROLL_PTR | 指向 Undo Log 的指針(用于回溯歷史版本),指向上一個版本 |
undo log
回滾日志,是一種邏輯日志但記錄的數據修改前的物理行數據值。是InnoDB引擎中實現事務原子性、一致性和MVCC的重要機制。記錄事務對數據的修改操作,用于事務回滾時提供撤銷修改的數據依據,或在快照讀時提供歷史版本數據。
-
undo log類型
- Insert undo log(插入回滾日志):僅用于記錄
INSERT
操作。- 記錄內容:插入的完整行數據(包括所有字段值)。
- 原因:插入的記錄在事務提交前,僅對當前事務可見,其他事務無法訪問。若事務回滾,只需通過 undo log 定位到這些插入的行,直接刪除即可(反向操作是 “刪除插入的行”,而 undo log 記錄行數據是為了精準定位要刪除的記錄)。
- Update undo log(更新回滾日志):用于記錄
UPDATE
和DELETE
操作(注:InnoDB 中DELETE
本質是標記刪除,也屬于特殊的更新)。- 記錄內容:被修改行的舊版本數據(包括所有字段值),而非抽象的 “反向操作邏輯”。
- 原因:更新 / 刪除操作會改變行的已有數據,回滾時需要恢復到修改前的狀態。例如,若將
age=20
改為age=30
,undo log 會記錄age=20
(舊值)以及未修改的字段數據,回滾時直接用舊值覆蓋新值即可;若刪除一行,undo log 會記錄該行刪除前的完整數據,回滾時重新插入該數據(恢復刪除)。
- Insert undo log(插入回滾日志):僅用于記錄
-
存儲方式
-
存儲在 InnoDB 的undo 表空間。
-
按 “段”管理,每個事務會分配一個或多個 undo log 段。
-
核心作用
-
事務回滾
- 當事務回滾
ROLLBACK
或數據庫崩潰,InnoDB
通過undo log
實現對數據修改的撤銷,恢復到事務開始時的狀態。 - 示例
BEGIN; UPDATE users SET balance = 100 WHERE id = 1; -- 記錄undo log(舊值balance=50) DELETE FROM orders WHERE id = 10; -- 記錄undo log(舊記錄完整信息) ROLLBACK; -- 執行undo log:balance恢復為50,orders表恢復id=10的記錄
- 當事務回滾
-
MVCC支持
- 快照讀(普通SELECT)時,InnoDB通過undo log獲取數據的歷史版本,確保事務執行過程中看到的是事務開始時的一致性視圖,不會受其他事務影響,避免了臟讀、不可重復讀、幻讀。
事務回滾支持
- 事務開始:分配undo log空間。
- 修改操作:每次執行寫操作,將舊的數據版本寫入undo log。
- 例如:
UPDATE t SET a=2 WHERE id=1
(原 a=1),undo log 記錄(id=1, a=1)
。
- 例如:
- 事務提交:
- INSERT undo log:直接標記為可刪除。
- UPDATE/DELETE undo log:保留,供其他事務的快照讀使用,由 purge 線程后續清理。
- 事務回滾:反向執行 undo log 中的操作(如將 a=2 恢復為 a=1), v 徹底撤銷事務影響。
MVCC支持
- 配合每行數據隱藏列DB_TRX_ID(記錄最后一次修改的事務ID)和DB_ROLL_PTR(指向undo log的指針)。
- 當事務需要獲取對應的數據版本時,通過DB_ROLL_PTR遍歷undo log獲取符合當前事務可見性的版本。
版本鏈
版本鏈是快照讀(普通SELECT
)實現一致性視圖的核心。
版本鏈的每個節點對應事務對某行的一次修改
,而非一個事務的多次修改。版本鏈是通過行記錄的 roll_ptr
指針和 undo log 記錄的 prev
指針串聯形成的,每一次修改都會生成一個新的 undo log 節點。
示例:
一張表的原始數據為:
id | age | name | DB_TRX_ID | DB_ROLL_PTR |
---|---|---|---|---|
30 | 30 | A30 | 1 | null |
四個并發事務同時訪問這張表
事務2 | 事務3 | 事務4 | 事務5 |
---|---|---|---|
開始事務 | 開始事務 | 開始事務 | 開始事務 |
修改age=3(id=30) | 查詢id=30的記錄 | ||
提交事務 | |||
修改name=A3(id=30) | |||
查詢id=30的記錄 | |||
提交事務 | |||
修改age=10(id=30) | |||
查詢id=30的記錄 | |||
查詢id=30的記錄 | |||
提交事務 |
當事務2執行修改時,創建最新的版本(age=3),舊數據會記錄在undo log日志,形成下圖版本鏈:
當事務3執行修改操作時,創建新的版本(name=A3),舊數據(age=3,非整行數據)會記錄在undo log,新版本DB_ROLL_PTR指向修改前舊版本:
當事務3執行修改操作時,創建新的數據版本,舊數據(age=3)記錄在undo log,新數據版本DB_ROLL_PTR指向修改前的舊版本:
不同事務或相同事務對同一記錄進行修改,會導致該記錄的undo log形成一條不同版本的版本鏈表,鏈表頭部是最新的數據版本,尾部是最早的數據版本。
Read View
一致性視圖,在事務開始時創建,記錄了事務啟動時活躍事務狀態。通過比對Read View中的參數和undo log中數據版本的事務ID,可以判斷事務在某時間點能看到的數據版本范圍,是事務內一致性讀的關鍵。
Read View的組成
Read View包含四個核心字段
字段 | 含義 |
---|---|
m_ids | 當前活躍的事務ID集合 |
min_trx_id | 最小活躍事務ID |
max_trx_id | 即將分配的事務ID,即當前最大事務ID+1 |
creator_trx_id | 創建當前Read View的事務ID |
活躍事務:指Read View創建時還未提交的事務。
創建Read View的時機
不同的隔離級別下創建Read View的時機也不同:
- RC:在事務每次快照讀時創建。
- RR:在事務第一次快照讀時創建,后續快照讀復用當前Read View。
判斷可見性
當事務訪問某一行數據時,會遍歷其undo log版本鏈,找到該事務可見的數據版本,trx_id是undo log版本鏈中的DB_trx_id(創建該版本的事務ID)。
判斷規則:
條件 | 是否可見 | 說明 |
---|---|---|
trx_id == creator_trx_id | 可見 | 該版本是當前事務自己修改的 |
trx_id < min_trx_id | 可見 | 該版本在Read View創建前就已提交 |
trx_id >= max_trx_id | 不可見 | 該版本在Read View創建后才創建 |
trx_id ∈ m_ids | 不可見 | 該版本由Read View創建時未提交的事務修改 |
trx_id ? m_ids 且 min_trx_id ≤ trx_id < max_trx_id | 可見 | 該版本在Read View創建時已提交 |
隔離級別的實現原理
事務隔離級別的實現是MVCC和鎖機制配合的結果。
涉及到的核心機制:
機制 | 作用 | 適用的隔離級別 |
---|---|---|
MVCC(undo log+ReadView ) | 實現非阻塞讀(快照讀),通過版本鏈提供一致性視圖 | RC,RR |
臨鍵鎖(間隙鎖+記錄鎖) | 鎖定索引間隙和記錄,防止插入和修改,解決幻讀、臟寫 | RR,S |
間隙鎖 | 鎖定索引間隙,防止插入,避免幻讀 | RR,S |
行鎖(記錄鎖) | 鎖定單行索引記錄,避免寫沖突(臟寫) | 所有寫操作 |
undolog | 事務回滾 |
讀未提交RU
核心特性:直接讀取最新的數據(包括未提交的數據變化)臟讀,所以RU的實現不依賴ReadView
。
- undo log的表現:
- 讀操作直接訪問最新的數據版本(包括未提交的修改)。
- undo log僅用于事務回滾。
- 鎖機制:
- 寫操作加排它鎖(X鎖),持鎖至事務結束,避免臟寫。
- 不會阻止該行的讀操作,讀操作不會加鎖,排它鎖只阻塞嘗試獲取鎖的操作。
- 讀操作(包括當前讀)不加鎖,導致臟讀。
- 寫操作加排它鎖(X鎖),持鎖至事務結束,避免臟寫。
讀已提交RC
核心特性:避免臟讀、不可重復讀問題、幻讀問題、讀已提交的最新數據版本。
- MVCC:
- 每次快照讀創建新的
ReadView
,保證每次讀取的都是最新的已提交版本,快照讀在undo log版本鏈找到事務可見的數據版本(當前快照讀時最新已提交的數據版本)。 - 因為使用
ReadView
,利用ReadView
中關于ReadView
創建時的參數(m_ids等)與undolog版本鏈的事務ID參數(DB_trx_ID)比對,能避免讀到活躍事務修改的數據版本,以此避免臟讀問題。 - 會因為每次快照讀都創建新的
ReadView
,每個Readview
可見的數據版本可能不同,造成不可重復讀的問題。
- 每次快照讀創建新的
- 鎖機制:
- 當前讀加記錄鎖,持鎖至事務結束,鎖定當前行,避免其他事物修改該行數據,造成臟寫。
- 不加間隙鎖,會出現幻讀。
可重復讀RR
-
MVCC:
- 第一次快照讀時創建
ReadView
,該事務內所有快照讀會在共用該ReadView
在undo log版本鏈上找到事務可見的數據版本(事務開始時已提交的數據版本),避免臟讀和不可重復讀。 - 只使用MVCC快照讀讀取固定的一個數據版本,不會出現幻讀問題。
- 第一次快照讀時創建
-
鎖機制:
- 當前讀使用臨鍵鎖,防止幻讀和臟寫。
- 只使用當前讀,或第一次讀操作是當前讀,會對查詢的數據范圍加臨鍵鎖,即便之后在鎖范圍內再使用快照讀也不會出現幻讀問題。但是如果之后的快照讀不在鎖定范圍并且又使用當前讀暴露了其他事務的修改,也會出現不可重復讀和幻讀。
-
仍存在的幻讀問題:
-
快照讀當前讀混合讀
- 由快照讀讀取事務開始時的數據版本變成讀取最新版本的當前讀,且中間有其他事務修改該數據。
-- 事務 A (RR) BEGIN; -- 快照讀:基于 MVCC 首次 Read View SELECT * FROM users WHERE age > 20; -- 返回 2 行 (id=30,40)-- 事務 B 插入并提交:INSERT INTO users(age) VALUES(25); -- id=50-- 當前讀:直接讀取最新數據(繞過 MVCC) SELECT * FROM users WHERE age > 20 FOR UPDATE; -- 返回 3 行 (id=30,40,50) COMMIT;
-
快照讀更新操作引發數據可見(隱式當前讀與快照讀混用)
- 更新使其他事務插入的行可見
-- 事務A (RR) BEGIN; SELECT * FROM users WHERE age>20; -- 快照讀:返回id=30 (age=30)-- 事務B:INSERT INTO users(age) VALUES(25); COMMIT;UPDATE users SET status=1 WHERE age>20; -- 當前讀:更新id=30和id=新行 SELECT * FROM users WHERE age>20; -- 看到id=30和id=新行
- 事務與其他事務更新不同列
-- 事務A (RR) BEGIN; SELECT * FROM users WHERE id=1; -- 看到(name='A', age=20)-- 事務B:UPDATE users SET name='B' WHERE id=1; COMMIT;-- 事務A更新不同列 UPDATE users SET age=21 WHERE id=1; -- 當前讀:基于(name='B', age=20)更新 SELECT * FROM users WHERE id=1; -- 看到(name='B', age=21)
- 其他事務刪除數據
-- 事務A (RR) BEGIN; SELECT * FROM users WHERE id=1; -- 看到數據-- 事務B:DELETE FROM users WHERE id=1; COMMIT;UPDATE users SET age=21 WHERE id=1; -- 0 rows affected(數據已不存在) SELECT * FROM users WHERE id=1; -- 無結果
- 本事務與其他事務修改不同行
-- 初始數據:id=1, col1=100, col2=200 -- 事務A (RR) | 事務B ----------------------|------------------- BEGIN; | SELECT col1 FROM t; | BEGIN; --> 100 || UPDATE t SET col2=300;| COMMIT; UPDATE t SET col1=150;| SELECT * FROM t; | --> col1=150, col2=300|
-
原因分析:
- RR 通過事務開始時固定的 ReadView 確保快照讀避免不可重復讀和幻讀。但更新操作(隱式當前讀)會繞過 ReadView 直接讀取最新數據版本,繼承其他事務的修改(包括插入/刪除/更新),并將修改后的數據以本事務 ID 寫入新版本。這導致:
- 若其他事務插入新行且匹配更新條件 → 幻讀
- 若其他事務更新同一行 → 不可重復讀
- 若其他事務刪除行且嘗試更新該行 → 行消失(不可重復讀)
- RR 通過事務開始時固定的 ReadView 確保快照讀避免不可重復讀和幻讀。但更新操作(隱式當前讀)會繞過 ReadView 直接讀取最新數據版本,繼承其他事務的修改(包括插入/刪除/更新),并將修改后的數據以本事務 ID 寫入新版本。這導致:
-
解決辦法:
- 讀操作使用加鎖讀,也是串行化的解決方案嗎,但業務中可考慮上述情況是否會出現。
-
串行化S
- MVCC:
- 禁止快照讀,所有讀裝換為當前讀。
- 鎖機制:
- 將普通讀操作隱式加**
SELECT ... FOR SHARE
(共享鎖)**。 - 每次讀操作都會對查詢范圍內的數據行和間隙加臨鍵鎖,徹底避免幻讀和不可重復讀。
- 將普通讀操作隱式加**
事務原理
Undo Log回滾
前像版本
- 事務回滾要將數據恢復到前像版本,而前像版本指的是數據行隱藏字段DB_ROLL_PTR指向的undo log版本鏈的直接前驅版本,從最新的修改開始執行create_trx_id是當前事務id的版本鏈的反向邏輯就能恢復行數據版本。
- DB_ROLL_PTR指向的版本鏈中的版本一定是在該版本創建時已提交的事務修改的,mysql的寫操作是隱式加鎖讀(當前讀),對同一數據行的寫操作事務一定是串行執行的。
- 除了可用于回滾的直接前驅版本,也就是更早版本,依然存在是MVCC給其他未提交且可見此版本的事務用于快照讀的。
不同類型的 Undo Log 中舊版本的存儲內容和回滾操作
操作類型 | 存儲內容 | 回滾操作 |
---|---|---|
UPDATE | 被修改前行數據的完整版本(含所有字段舊值) | 用 undo log 中記錄的舊值覆蓋當前行數據,恢復 DB_TRX_ID 和 DB_ROLL_PTR 為修改前的狀態,撤銷字段更新。 |
DELETE | 整行數據的完整版本(含所有字段舊值,相當于特殊更新的舊狀態) | 清除行的刪除標記(DELETE_BIT ),用 undo log 中的舊值恢復行數據可見性,DB_TRX_ID 和 DB_ROLL_PTR 回退到刪除前的版本。 |
INSERT | 新插入行的完整主鍵信息(主鍵值及元數據) | 根據主鍵定位到插入的行,執行物理刪除(因插入行未提交,其他事務不可見,刪除后無殘留)。 |
回滾核心流程:逆向遍歷 undo log 并執行反向操作
回滾過程會從事務的最后一個修改操作開始,逆向遍歷事務的 undo log 鏈表,逐個對每個操作執行 “反向邏輯”,直到所有修改被撤銷。具體步驟如下:
步驟 1:定位事務的 undo log 鏈表
InnoDB 通過事務 ID 找到該事務對應的 undo log 鏈表,鏈表的 “頭節點” 是事務最后一次修改生成的 undo log 記錄,“尾節點” 是事務第一次修改生成的 undo log 記錄。
步驟 2:從最后一個修改開始逆向處理
回滾按 “逆序” 處理每個 undo log 記錄(即先撤銷最后執行的操作,再撤銷倒數第二個,以此類推),確保數據恢復的正確性。以下按操作類型分述:
場景 1:撤銷 INSERT 操作(基于 Insert undo log)
- undo log 內容:記錄了插入行的完整數據(含主鍵)。
- 反向操作:根據 undo log 中的主鍵定位到插入的行,直接刪除該行(因為插入的行在事務提交前僅對當前事務可見,刪除后其他事務無法感知)。
- 示例:事務內執行
INSERT INTO user VALUES (1, '張三')
,回滾時通過 Insert undo log 找到id=1
的行,執行刪除。
場景 2:撤銷 UPDATE 操作(基于 Update undo log)
- undo log 內容:記錄了被修改行的完整舊版本數據(修改前的所有字段值)。
- 反向操作:根據 undo log 中的主鍵定位到數據行,用舊版本數據覆蓋當前版本(即恢復
DB_TRX_ID
為舊版本的事務 ID,DB_ROLL_PTR
指向舊版本的前驅 undo log)。 - 示例:事務內先執行
UPDATE user SET age=30 WHERE id=1
(原 age=20),回滾時通過 Update undo log 找到id=1
的行,將 age 恢復為 20,DB_ROLL_PTR
指向修改前的舊版本 undo log。
場景 3:撤銷 DELETE 操作(基于 Update undo log)
- undo log 內容:記錄了被刪除行的完整舊版本數據(刪除前的所有字段值)。
- 反向操作:根據 undo log 中的主鍵定位到被標記刪除的行,恢復其數據為舊版本(清除刪除標記
delete_flag
),并更新DB_TRX_ID
和DB_ROLL_PTR
為舊版本信息。 - 示例:事務內執行
DELETE FROM user WHERE id=1
,回滾時通過 Update undo log 找到id=1
的行,恢復其數據(取消刪除標記),使其可見性恢復到刪除前的狀態。
初始狀態
賬戶表 (accounts)
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | Alice | 1000.00 |
| 2 | Bob | 500.00 |
+----+-------+---------+
事務操作序列
BEGIN; -- 事務A開始
-- 操作1:Alice轉出100
UPDATE accounts SET balance = 900.00 WHERE id = 1;
-- 操作2:Bob轉入100
UPDATE accounts SET balance = 600.00 WHERE id = 2;
數據頁未持久化
初始狀態
回滾流程
結果:
- 內存數據恢復為每項修改數據的前像
- 磁盤數據保持 (無需操作)
- 無磁盤 I/O 發生
數據頁全部持久化
初始狀態
操作1持久化:Page1已刷盤 → id=1, balance=900
操作2持久化:Page2已刷盤 → id=2, balance=600Undo Log:記錄1: id=1, old_balance=1000記錄2: id=2, old_balance=500
回滾流程
關鍵步驟詳解:
- 內存回滾
- 立即將內存中的數據恢復為前像值
- 緩沖池標記為臟頁(因為與磁盤不一致)
- Redo Log 保護
- 保證回滾操作本身的持久性
- 數據頁刷盤
- 后臺線程將恢復后的數據刷到磁盤
- 刷盤過程依然通過 Doublewrite 防止頁斷裂
部分數據頁持久化
初始狀態
操作1持久化:Page1已刷盤 → id=1, balance=900
操作2未持久化:Page2在內存 → id=2, balance=600 (臟頁)Undo Log:記錄1: id=1, old_balance=1000記錄2: id=2, old_balance=500
回滾流程
關鍵步驟詳解:
- 內存回滾
- 不論是否持久化都將內存中的數據恢復為前像值
- 已經持久化的數據頁將緩沖池標記為臟頁(因為與磁盤不一致)
- 未持久化的數據頁將臟頁標識去除(與磁盤數據一致,無需刷盤)
- Redo Log 保護
- 保證回滾操作本身的持久性
- 數據頁刷盤
- 后臺線程將恢復后的數據刷到磁盤
- 刷盤過程依然通過 Doublewrite 防止頁斷裂
Redo Log
-
重做日志,記錄的是事務提交時數據頁的物理修改,在刷新臟頁到磁盤中發生錯誤時或數據庫崩潰時,用于數據恢復,以實現事務持久性。
-
組成:Redo Log Buffer(重做日志緩沖)和Redo Log File(重做日志文件),前者在內存中,后者在磁盤中
-
臟頁:在執行事務的增刪改操作時會先對內存中的
Buffer Pool
緩沖池進行修改,如果緩沖池中不存在則會由后臺線程將數據從磁盤中讀出存放在緩沖池中,并對數據進行修改,修改后的數據頁就稱為臟頁(與磁盤中數據不一致)。 -
Redo Log解決的問題:后臺線程會在一定時機將臟頁刷新到磁盤中,但刷新不是實時的,如果事務已提交并返回成功,但是如果在未成功刷盤時出錯或崩潰
- 導致已提交的事務丟失,事務的持久性就未能保證。
- 未提交的事務的部分數據頁被刷新到磁盤中,導致數據不一致。
WAL日志先行
-
日志先行,所有的數據頁修改前必須先將對應的修改記錄寫入到日志,并保證日志落盤以保證事務的持久性。
-
工作流程
-
事務執行階段
事務在修改數據頁時會同步生成redo log記錄(包括表空間ID、頁號、偏移量、修改值等物理信息)
- 物理邏輯日志:記錄頁級別的物理修改,而非 SQL 語句
- 實時生成:每條數據修改都立即產生日志
- 內存緩沖:日志暫存內存,未直接落盤
- 設計目的:利用內存緩沖避免每次修改都觸發磁盤 I/O,大幅提升事務執行效率。
- 事務提交階段
- 事務提交時,根據
innodb_flush_log_at_trx_commit
參數決定日志的刷盤策略。- 策略1(默認安全):立即將 Log Buffer 中的日志刷到磁盤文件
- 策略2(平衡):僅寫入操作系統緩存
- 策略0(高性能):依賴后臺線程異步刷盤
- 保證事務提交時,相關redo log至少進入操作系統持久化層,滿足事務的持久化要求。
- 事務提交時,根據
- 后臺處理階段
- 當日志文件寫滿 75% 時,觸發 Checkpoint(檢查點)
- 將內存中最早的臟頁刷入磁盤
- 更新系統表空間中的
checkpoint_lsn
- 回收已刷盤日志的存儲空間
- 崩潰恢復階段
- 定位系統表空間中的
checkpoint_lsn
(最近一次刷盤成功的點) - 從LSN開始掃描Redo Log文件
- Redo重做:重新應用Redo Log中的所有日志記錄,恢復數據頁狀態。
- 注意:Redo重做操作并不是直接去修改磁盤上的數據頁,而是將redolog記錄的修改應用到緩沖池中對應的數據頁上。如果緩沖池中沒有對應的數據頁,則從磁盤讀取到緩沖池,然后在緩沖池中應用Redo Log的修改。
- Undo回滾:根據Undo Log回滾所有未提交事務的修改(這些事務無法繼續完成,回滾保證一致性)。
- 未持久化修改:恢復內存數據前像,與磁盤數據一致,去除臟頁標識。
- 已持久化修改:恢復內存數據前像,與磁盤數據不一致,標記臟頁,添加回滾Redo,刷盤后將數據恢復值前像版本。
- 定位系統表空間中的
- 如果不應用redo log,那么想保證事務的持久性,就要在事務提交時,將所有被該事務修改的臟頁同步到磁盤中,這些臟頁可能在磁盤中分散的位置,所以同步操作會涉及到大量的隨機磁盤IO。
- WAL日志先行的機制下,讀數據頁的修改會以日志形式記錄在redo log buffer,在事務提交時再將日志持久化到redo log文件中,而寫入redolog文件的操作是追加寫,只是一種高效的順序寫IO。
- 在redolog持久化到磁盤后,事務的持久性就已經被保證,即使數據庫崩潰也可以依靠redo重放來恢復修改,所以緩沖池中臟頁的刷盤就可以是
- 延遲的:
- 降低提交延遲,用戶能更快得到提交成功的響應,
- 增加合并機會,讓后續可能對一個頁的修改在緩沖池中合并,最終只刷一次盤。
- 批量的:
- 分攤磁盤IO開銷,一次磁盤IO的時間成本被分攤到了多個數據頁上,平均每個頁的IO成本降低。
- 分攤系統調用開銷,一次系統調用的成本被多個數據頁分攤。
- 可優化的
- 操作系統IO調度器,會嘗試對批量的請求進行排序(如類似電梯算法 - SCAN或C-SCAN),使磁頭移動路徑更短,減少隨機磁盤IO的性能損耗。
- 延遲的:
WAL將隨機數據修改轉化為順序日志寫入,避免每次修改都觸發磁盤 I/O,大幅提升事務執行效率,并且延遲刷盤可以增加臟頁修改合并機會。
-
事務原理實現
原子性 (Atomicity): “要么全做,要么全不做”
- 核心機制: Undo Log
- 實現過程:
- 執行任何修改(
INSERT/UPDATE/DELETE
)前,先在 Undo Log 中記錄修改前的數據狀態(舊值或反向操作邏輯)。(注意:寫入 Undo Log 本身也是一個修改,會被 Redo Log 記錄以保證 Undo Log 的持久性)。 - 修改內存中的數據頁(產生臟頁)。
- 提交 (Commit):
- 生成包含
COMMIT
標記的 Redo Log 記錄并 強制刷盤 (fsync
)。(此時持久性已保證) - 臟頁異步刷盤。
- 生成包含
- 回滾 (Rollback) / 失敗:
- 引擎根據 Undo Log 中的記錄,執行邏輯逆操作(如
DELETE
的逆操作是INSERT
,UPDATE
是恢復舊值),將數據恢復到事務開始前的狀態。
- 引擎根據 Undo Log 中的記錄,執行邏輯逆操作(如
- 關鍵點: Undo Log 提供了將事務所有修改“撤銷”回去的能力。無論提交還是回滾,事務內的操作被視為一個不可分割的整體。Redo Log 保證了 Undo Log 操作本身的可靠性。
- 執行任何修改(
一致性 (Consistency): “數據庫總是從一個一致狀態轉換到另一個一致狀態”
- 核心機制: ACID 共同目標 + 數據庫約束 + 應用邏輯
- 實現過程:
- 原子性 確保事務邊界內的轉換是原子的,不會停留在中間不一致狀態。
- 隔離性 防止并發事務看到彼此未完成的不一致修改。
- 持久性 確保提交的狀態是永久的,不會因崩潰丟失導致狀態回退。
- 數據庫約束 (主鍵、外鍵、唯一、非空、CHECK):在事務執行過程中(通常在語句級或事務提交時)進行校驗。違反約束的操作會被拒絕,觸發回滾(依賴 Undo Log)。
- 應用邏輯:業務規則需要開發者在事務代碼中正確實現。
- 關鍵點: A、I、D 是實現 C 的基礎手段。Undo Log 在回滾違反約束的操作、MVCC 在提供一致性讀視圖上都對一致性有直接貢獻。
隔離性 (Isolation): “并發執行的事務相互隔離,感覺像串行執行”
- 核心機制: 鎖機制 + MVCC (基于 Undo Log)
- 實現過程:
- 寫-寫沖突 (核心:鎖機制):
- 當一個事務要修改某數據項時,必須先獲得相應的鎖(如行鎖、X鎖)。
- 其他事務試圖修改同一數據項時會被阻塞(或根據隔離級別報錯),直到鎖釋放。這保證了同一時間只有一個事務能修改特定數據,防止數據被并發寫破壞。
- 例如(Repeatable Read):事務A修改行R時加X鎖,事務B嘗試修改R會被阻塞直到A提交/回滾釋放鎖。
- 讀-寫沖突 (核心:MVCC + Undo Log):
- MVCC 基礎: 每行數據包含隱藏字段
DB_TRX_ID
(最后修改它的事務ID)和DB_ROLL_PTR
(指向該行在 Undo Log 中舊版本記錄的指針),形成數據行的版本鏈。 - 快照讀 (非鎖定讀): 當讀操作發生時(在 RC 或 RR 級別下):
- 系統根據事務啟動時刻(或語句開始時刻,取決于隔離級別)生成一個 Read View。Read View 包含當時所有活躍(未提交)事務ID列表。
- 通過
DB_ROLL_PTR
遍歷版本鏈。 - 找到滿足以下條件的版本:
- 創建該版本的事務ID
<
Read View 中最小活躍事務ID (說明該版本在事務開始時已提交)。 - 或 創建該版本的事務ID 在 Read View 中但等于自身事務ID (說明是自己修改的)。
- 且 該版本的
DB_TRX_ID
是鏈中滿足上述條件的最大值 (即該事務開始時能看到的最新已提交版本)。
- 創建該版本的事務ID
- 讀取該版本的數據(存儲在 Undo Log 中)。讀操作不阻塞寫操作,寫操作也不阻塞讀操作。
- 例如(Repeatable Read):事務A開始時生成Read View V1。事務B在A之后修改并提交了行R。事務A再次讀R時,通過V1和Undo Log鏈,仍然會讀到B修改前的版本(快照)。
- MVCC 基礎: 每行數據包含隱藏字段
- 關鍵點: 鎖機制 直接處理并發寫,強制串行化寫操作。MVCC 利用 Undo Log 提供的歷史版本,為讀操作提供一致性視圖,解決了讀寫沖突,極大提高了并發讀性能。不同的隔離級別(RC, RR)主要通過調整 Read View 的生成時機(語句級/事務級)和鎖的范圍(如 RR 的間隙鎖)來實現。Redo Log 保證了 Undo Log 版本鏈的持久性,支撐 MVCC 在崩潰恢復后仍有效。
- 寫-寫沖突 (核心:鎖機制):
持久性 (Durability): “一旦事務提交,修改永久保存”
- 核心機制: Redo Log + WAL 原則
- 實現過程:
- 事務提交時,其產生的所有修改操作(包括數據修改和 Undo Log 的寫入)對應的 Redo Log 記錄(物理日志),以及一個標識事務提交的
COMMIT
記錄,必須被 強制刷盤 (fsync
) 到持久化存儲(Redo Log File)中。這是 WAL 原則的核心要求。 - 此時,即使系統立即崩潰,這些修改操作已安全保存在磁盤上。
- 內存中被修改的數據頁(臟頁)不需要在提交時立即刷盤。數據庫會在后臺選擇合適的時間(Checkpoint 機制),將臟頁批量、異步地寫回磁盤數據文件。這極大提高了性能(將隨機寫轉化為順序寫 + 延遲批量刷臟頁)。
- 崩潰恢復:
- 數據庫重啟時,首先定位到 Redo Log 中最近的 Checkpoint(記錄了當時哪些臟頁已刷盤)。
- 從 Checkpoint 開始掃描 Redo Log。
- 重做 (Redo): 重新執行所有 Checkpoint 之后、日志末尾之前的、且帶有
COMMIT
標記的 Redo Log 記錄對應的操作。這確保了所有已提交事務的修改都被重新應用到數據文件。 - 回滾 (Undo): 對于 Redo Log 中存在但沒有
COMMIT
標記的事務(崩潰時未提交的事務),利用 Undo Log 進行回滾(原理同原子性中的回滾),撤銷這些未完成事務的修改。
- 關鍵點: 強制刷盤 Redo Log (含Commit標記) 是持久性的絕對保證。異步刷臟頁是性能優化。崩潰恢復中的 Redo 階段確保了已提交修改不丟失,Undo 階段(依賴 Undo Log)保證了未提交修改被清除,共同維護了數據庫狀態的一致性。Undo Log 本身的寫入也受 Redo Log 保護。
- 事務提交時,其產生的所有修改操作(包括數據修改和 Undo Log 的寫入)對應的 Redo Log 記錄(物理日志),以及一個標識事務提交的