MySQL
事務
事務是一組不可分割的操作集合,這些操作要么同時成功提交,要么同時失敗回滾。
acid事物的四大特性
原子性
最小工作單元,要么同時成功,要么同時失敗。
例如A轉賬300給B,A賬戶-300與B賬戶+300必須滿足操作原子性,避免出現A已轉賬但B未收到的一致性問題。
一致性
事務操作的起點和終點必須是從一個一致性狀態到另一個一致性狀態,也就是數據庫的數據變化必須符合預定義期望變化。(不會出現一個數據庫修改成功、一個失敗的情況)
例如在轉賬案例中事務開始時的賬戶總額等于事務結束時的賬戶金額。(并不是一定相等,數據變化符合業務預定義期望即可)
隔離性
并發的事務是相互隔離的。
例如多個并發轉賬事務,每個轉賬操作的數據是相互獨立的,不會出現數據混亂的情況。
持久性
一旦事務提交,其結果就是永久的,不會因系統崩潰丟失。
事務提交后會將數據持久化到硬盤,例如在裝張案例中,變更后賬戶數據持久化在硬盤,數據庫崩潰依然被保留。
并發事務問題
臟讀
事務A讀取到事務B未提交的修改。
不可重復讀
同一事務內多次讀取同一數據時因為其他事物在此期間提交了數據修改導致結果不同。
幻讀
同一事務內對一張表的查詢結果集不同,因為其他事務在此期間插入刪除了數據。
select * 結果集行數不同。
select count()/sum() 等聚合函數,查詢內容可能不同。
例如,事務A查詢name=張三不存在,事務B插入張三,事務A按照張三不存在的業務邏輯插入張三但無法插入。
隔離級別
讀未提交(RU)
允許事務讀取其他事物未提交的修改(臟讀)。
并發性能最高。
讀已提交(RC)
不允許事務讀取其他事物未提交的修改(臟讀)。
無法避免不可重復讀現象。
可重復讀(RR)
不會出現臟讀和不可重復讀問題。
無法避免幻讀問題。
MySQL默認隔離級別。
串行化(S)
完全避免所有并發問題。
并發性能最低。
如何選擇隔離級別
隔離級別越高,并發性能越低。
-
讀未提交(RU):僅適用對數據準確性要求極低,并發性能要求極高的場景,如監控數據,日志采集,瞬時數據不影響整體的場景,但實際實際生產環境下中還是極少使用,規避臟讀風險。
-
讀已提交(RC):適用大部分普通業務場景,也是大部分數據庫的默認隔離級別。例如用戶信息頁,用戶A修改提交后,用戶B刷新就能看到用戶A提交的修改內容,但不會看到用戶A未提交的內容。
RC下不可重復讀問題:
🛒 場景一:庫存扣減(并發搶購)
-
業務邏輯: 用戶下單時,需要檢查并扣減商品庫存(例如商品A,初始庫存10件)。
-
事務A (用戶1下單):
BEGIN;
(RC隔離級別)SELECT stock FROM products WHERE id = 'A';
// 返回 10 (庫存充足)- (基于查詢結果10,決定繼續下單邏輯… 生成訂單、計算價格等,耗時幾毫秒/秒)
-
事務B (用戶2下單): (幾乎與事務A同時發生)
BEGIN;
(RC隔離級別)SELECT stock FROM products WHERE id = 'A';
// 也返回 10 (庫存充足)UPDATE products SET stock = stock - 1 WHERE id = 'A';
// 扣減1件,庫存變為9COMMIT;
// 用戶2下單成功,庫存更新為9并生效
-
事務A 繼續執行:
- (執行完其他邏輯后)
UPDATE products SET stock = stock - 1 WHERE id = 'A';
// 此時基于 *當前已提交數據* (stock=9) 扣減,庫存變為8COMMIT;
// 用戶1下單成功
-
問題:
- 兩個用戶都成功下單購買了商品A。
- 最終庫存變為 8,這符合物理扣減。
-
不可重復讀在哪里?
- 事務A 在步驟2讀取
stock=10
。 - 在它執行后續邏輯時,事務B 修改并提交了庫存(變為9)。
- 當事務A 執行更新操作(步驟2.2)時,它沒有基于自己最初讀到的10去減1,而是基于最新已提交值9去減1。雖然最終庫存正確(8),但事務A在邏輯判斷(庫存是否充足)后,執行更新操作時依賴的數據(庫存值)已經發生了變化(10 -> 9)。這就是一次“不可重復讀”(在同一個事務A內,如果它再次執行
SELECT stock...
,結果會是9,而不是最初的10)。
- 事務A 在步驟2讀取
-
潛在風險:
-
超賣風險: 如果初始庫存只有1件,多個事務都讀到1(認為充足),然后都去扣減1(事務B扣成0并提交,事務A再基于0扣減就會變成-1)。這就是經典的并發超賣問題!雖然RC下避免了臟讀(不會讀到事務B未提交的扣減),但因為不可重復讀,兩個事務都基于“過時”的充足判斷進行了扣減,導致庫存為負。解決超賣通常需要額外的并發控制(如樂觀鎖、悲觀鎖、Redis分布式鎖等),而不僅僅是依賴隔離級別。
-
🕒 場景二:預約系統(時間段占用檢查)
- 業務邏輯: 用戶預約某個資源(如會議室A在10:00-11:00時段)。
- 事務A (用戶1預約):
BEGIN;
(RC隔離級別)SELECT COUNT(*) FROM bookings WHERE room = 'A' AND start_time < '11:00' AND end_time > '10:00';
// 返回 0 (表示10:00-11:00空閑)- (用戶1填寫預約信息,點擊確認… 耗時幾秒)
- 事務B (用戶2預約): (幾乎與事務A同時發生,且操作更快)
BEGIN;
(RC隔離級別)SELECT ...
// 同樣返回0 (空閑)INSERT INTO bookings (room, start_time, end_time, user) VALUES ('A', '10:00', '11:00', 'user2');
// 插入預約記錄COMMIT;
// 用戶2預約成功
- 事務A 繼續執行:
- (用戶1點擊確認)
INSERT INTO bookings (room, start_time, end_time, user) VALUES ('A', '10:00', '11:00', 'user1');
// 嘗試插入- (可能成功也可能失敗,取決于唯一性約束)
COMMIT;
- 問題:
- 事務A和事務B都檢查了同一時間段,都認為它是空閑的(SELECT返回0)。
- 事務B更快地插入記錄并提交。
- 事務A隨后也嘗試插入記錄。
- 不可重復讀在哪里?
- 事務A在步驟2執行SELECT查詢,得知會議室A在10:00-11:00空閑。
- 在它執行插入操作之前,事務B已經插入并提交了占用該時間段的記錄。
- 當事務A執行插入操作時,它所依賴的“空閑”狀態(SELECT的結果)已經不再成立(因為事務B的插入已提交)。事務A在邏輯判斷(是否空閑)后,執行插入操作時依賴的數據狀態(時間段是否被占用)已經發生了變化。如果表上有
(room, start_time, end_time)
的唯一約束,事務A的插入會失敗(主鍵/唯一鍵沖突)。如果沒有唯一約束,則會產生雙重預訂!
- 潛在風險:
- 雙重預訂: 最嚴重的后果!同一個時間段被預約給了兩個用戶,導致沖突和用戶投訴。解決雙重預訂通常需要更嚴格的并發控制,如對目標時間段加行鎖(SELECT FOR UPDATE)或使用樂觀鎖(版本號)。
-
-
可重復讀(RR):適用同一事務內涉及一個以上對同一數據的查詢,業務要求不能使兩次查詢結果不一致。
幻讀問題典型案例
假設存在一張
goods
表,存儲商品庫存信息,初始數據如下:id name stock 1 手機 10 2 電腦 5 現在有兩個并發事務:事務 A 負責查詢并修改庫存小于 10 的商品,事務 B 負責插入一條新的庫存小于 10 的商品記錄。
步驟 1:事務 A 啟動并首次查詢
事務 A 開始,執行查詢 “庫存小于 10 的商品”:
-- 事務 A BEGIN; -- 第一次查詢:查詢庫存 < 10 的商品 SELECT * FROM goods WHERE stock < 10;
此時結果為:
id name stock 2 電腦 5 步驟 2:事務 B 插入新數據并提交
事務 B 啟動,插入一條新商品記錄(庫存 8,符合
stock < 10
),并提交事務:-- 事務 B BEGIN; -- 插入一條新商品,庫存 8(符合 stock < 10) INSERT INTO goods (name, stock) VALUES ('平板', 8); COMMIT;
此時表中數據變為:
id name stock 1 手機 10 2 電腦 5 3 平板 8 步驟 3:事務 A 再次查詢并嘗試修改
事務 A 再次執行相同的查詢:
-- 事務 A -- 第二次查詢:再次查詢庫存 < 10 的商品 SELECT * FROM goods WHERE stock < 10;
在 RR 隔離級別下,由于 MVCC 的可重復讀特性,事務 A 第二次查詢的結果仍為:
id name stock 2 電腦 5 但此時如果事務 A 嘗試修改 “所有庫存 < 10 的商品”(例如批量增加庫存):
-- 事務 A -- 嘗試修改所有庫存 < 10 的商品 UPDATE goods SET stock = stock + 2 WHERE stock < 10; COMMIT;
執行后,事務 A 查看最終數據時會發現:新插入的 “平板”(id=3)的庫存也被修改為 10(8+2)。
這就是幻讀:事務 A 兩次查詢都沒看到 “平板”,但修改操作卻影響了它,仿佛數據 “憑空出現” 并被修改。-
在RR級別下,不可重復讀場景能被解決,但依然會出現更新操作前判斷失效的情況,update是當前讀會直接讀取最新數據修改,依然會出現同時判斷成功的超賣問題。
-
場景一:庫存扣減
RR下的行為:
1.事務A開始并創建快照,執行
SELECT stock...
讀取的始終是快照中的庫存值(如10)2.事務B開始并執行扣減庫存,此時數據庫中stock值為9
3.事務A開始執行扣減庫存操作
UPDATE stock = stock - 1
但會讀取到被修改后的最新數據修改。結果:事務AB庫存判斷成功雖然解決了不可重復讀問題但還是會導致超賣。
解決方法:樂觀鎖、悲觀鎖、分布式鎖、庫存判斷加
For UPDATE
<select id="selectStockForUpdate" resultType="com.example.Goods">SELECT id, stock FROM goods WHERE id = #{id} FOR UPDATE <!-- 關鍵:對查詢到的行加排他鎖 --> </select>
-
-
串行化(S):事務串行化執行,適用RR下會出現幻讀且業務不允許的場景及事務必須嚴格按照提交順序執行的場景。
風險:事務A先開啟但是在未提交的情況下,事務B開啟并檢測B的余額,業務邏輯上B用戶賬戶余額一定滿足>=50,但是在RC,RR情況下事務A未提交所以事務B可能產生誤判。
串行化解決方案:
- 嚴格順序執行:
- 事務A完全執行后,再執行事務B
- 或事務B完全執行后,再執行事務A
- 嚴格順序執行:
存儲引擎
InnoDB
- 事務:支持 ACID(原子性、一致性、隔離性、持久性)。
- 外鍵:強制維護數據之間的邏輯關聯。
- 行級鎖:對特定行數據加鎖,同一表內其他行仍然可以并發讀寫。提高了系統吞吐量。
MyISAM
- 無事務
- 無外鍵
- 表級鎖
- 讀取性能更高(被MongoDB替代,MongoDB文檔級鎖(類似行級鎖)比MyISAM的表級鎖插入性能更高)
MEMORY
- 內存:所有數據存儲在RAM中,讀寫速度極快。
相對于Redis有以下局限性:
- 易失性:服務器宕機、重啟丟失所有數據。
- 功能單一:支持的數據結構簡單,缺乏Redis豐富的數據結構(列表、集合、哈希、位圖、流等)。
- 無分布式支持:難以構建分布式架構。
特性 | InnoDB (MySQL) | MyISAM (MySQL) | MEMORY (MySQL) | MongoDB | Redis |
---|---|---|---|---|---|
存儲模型 | 關系型 (行存儲) | 關系型 (行存儲) | 關系型 (行存儲, 內存中) | 文檔型 (BSON) | 鍵值對 + 多種高級數據結構 |
核心優勢 | ACID 事務, 行級鎖, 外鍵, 數據一致性強 | 讀取插入性能高(簡單查詢, COUNT(*)) | 內存速度讀寫 (臨時數據) | 靈活模式, 水平擴展, 高并發寫入 (文檔鎖) | 內存速度, 豐富數據結構, 持久化, 分布式 |
主要劣勢 | 相對復雜, 寫并發優化需技巧 | 表級鎖(并發寫入差), 無事務, 崩潰易損 | 易失性(重啟丟數據), 功能單一 | 弱于復雜 JOIN/事務 (雖有提升), 學習曲線 | 內存成本高, 不適合存超大對象/全量持久化 |
鎖機制 | 行級鎖 (寫) | 表級鎖 (寫) | 表級鎖 | 文檔級鎖 (現代引擎) | 單線程處理命令 (避免鎖競爭) |
持久性 | 強 (WAL 日志) | 弱 (崩潰易損) | 無 (純內存) | 可配置 | 可配置 (RDB/AOF) |
事務 | 完整 ACID 支持 | 不支持 | 不支持 | 支持多文檔 ACID 事務 (現代版本) | 支持簡單事務/樂觀鎖, 非完整 ACID |
擴展性 | 主從復制, 分片(較復雜) | 主從復制 | 無 | 內置強大分片 (Sharding) | 內置集群 (Redis Cluster) |
典型場景 | OLTP, 需要強一致性事務的應用 (支付, 訂單) | 讀取密集型應用, 靜態數據, 日志(歷史) | 臨時表, 緩存(簡單) | 讀取插入頻繁、更新刪除少” 場景 | 緩存, 會話存儲, 排行榜, 消息隊列, 實時系統 |
索引
索引是幫助MySQL高效獲取數據的數據結構。數據庫維護著指向存儲數據的數據結構和查找算法,以快速定位到查詢的數據。
核心作用
加速檢索:大幅減少掃描量,避免全表掃描。
加速排序和分組:利用索引的有序性可以極大提高GROUP BY
和ORDER BY
的速度。
代價
占用存儲空間:存儲索引需要額外的存儲空間。
降低寫操作速度:要維持索引的有序性,UPDATE
INSERT
DELETE
操作會增加所需的時間開銷。操作涉及的索引越多,時間消耗越大。
索引結構
二叉樹做索引的問題
二叉搜素樹
- 退化風險:用二叉樹做索引的數據結構會導致二叉搜素樹結構取決于插入順序,可能使樹的層級過深,極端情況下(順序插入)會退化成鏈表,增加查詢的掃描量。
- 無法磁盤預讀:二叉搜索樹分散節點存儲,每個節點只存一個數據,查詢過程中每個數據的訪問都涉及到一次磁盤IO,而磁盤IO正是外存訪問中最耗時的環節。
紅黑樹
- 避免退化:紅黑樹會維持樹的平衡,可以避免二叉搜索樹的退化問題。
- 無磁盤預讀:存儲與二叉搜索樹一樣分散存儲,一個節點只存儲一個數據,涉及大量磁盤IO。
B-樹和B+樹
B-樹
原理:多路平衡搜索樹
核心特性:
- 每個節點存儲多個鍵值和子節點指針。
- 節點大小為磁盤頁大小。
- 樹高遠低于二叉樹。
優勢:
- 減少磁盤IO:由于節點大小為磁盤頁大小,所以在檢索過程中可以完整讀取一個節點的所有鍵值進行比較檢索,顯著降低了磁盤IO的次數。
- 局部范圍查詢:節點內鍵值有序,可直接用于范圍查詢。
B+樹
B+樹在B-樹的基礎上做了更加適配數據庫索引的優化。
-
B+樹的節點不存儲數據,僅在葉子結點存儲數據。
-
B+樹的葉子結點使用雙向鏈表相連。
特性 B-樹 B+樹 數據存儲位置 所有節點均可存數據 僅葉子節點存數據 葉子節點結構 不鏈接 通過指針雙向鏈表串聯 非葉子節點功能 存鍵值 + 數據 僅存鍵值(索引導航作用) 鍵值冗余 無 鍵值在非葉節點重復出現(冗余)
B+樹的關鍵優化:
- 層級更低:B+樹中的非葉子節點不存儲數據,固定為磁盤頁大小的節點空間全部用于存儲鍵值和子節點的指針,因此將層級進一步壓縮。
- 范圍查詢更加高效:B+樹將存儲數據的葉子結點用雙向鏈表相連,范圍查詢只需查到起始鍵后遍歷鏈表至結束鍵,避免里B-樹范圍查詢時上下層級的切換。
- 全表掃描更快:順序遍歷葉子結點即可獲得全量數據,無需樹遍歷。
索引分類
按物理存儲方式分
維度 | 聚集索引 | 二級索引 |
---|---|---|
存儲內容 | 完整數據行 | 索引列 + 主鍵值 |
數量 | 1 個/表 | 多個/表 |
查詢速度 | 主鍵查詢極快 | 需回表,可能慢 |
葉子節點結構 | 數據行 | (索引列, 主鍵) + 指向主鍵的指針 |
依賴關系 | 數據物理存儲依據 | 依賴聚集索引的主鍵值 |
聚集索引下掛的完整的行數據。
聚集索引選取規則:
如果存在主鍵,主鍵索引就是聚集索引。
如果不存在主鍵,將使用第一個唯一(UNIQUE)索引作為聚集索引。
如果表沒有主鍵,或沒有合適的唯一索引,則InnoDB會自動生成一個rowid作為隱藏的聚集索引。
二級索引的葉子結點下掛的是鍵值和對應的主鍵值。
通過二級索引檢索數據時,先檢索出二級索引下對應鍵值下的主鍵值,再回表查詢出對應完整行數據。
按邏輯功能分
主鍵索引:每個表有且只有一個,值唯一且不為空,建表時自動創建,屬聚集索引。
唯一索引:允許存在多個,值唯一但允許有一個NULL,屬于二級索引。
常規索引:允許存在多個,只可重復且可為空,屬于二級索引。
前綴索引
基本原理
-
傳統索引:存儲字段的完整值。
-
前綴索引:僅存儲字段的前 N 個字符。
-- 為email字段的前5個字符創建前綴索引 CREATE INDEX idx_email_prefix ON users(email(5));
核心優勢
(1)減少索引體積
- 示例:若
email
字段平均長度為 20 字節,前綴索引(5 字節)可節省 75% 的存儲空間。 - 效果:更小體積的索引可使內存中留出更多空間緩存更多數據頁,減少磁盤 IO。
(2)提升寫入性能
- 因索引體積小,插入 / 更新時的索引維護開銷降低,頁分裂次數降低。
前綴長度選擇:
- 過短:區分度不足,導致索引掃描行數和回表查詢次數激增。
- 過長:失去空間優勢,性能接近完整索引。
- 建議通過
COUNT(DISTINCT LEFT(field, N))
/COUNT(*)測試不同長度的區分度。
-- 計算email前5個字符的唯一值占比
SELECT COUNT(DISTINCT LEFT(email, 5)) / COUNT(DISTINCT email) AS prefix_coverage
FROM users; -- 結果接近1時,說明前5個字符區分度高
聯合索引
創建語句:CREATE INDEX idx_abc ON table(a, b, c);
通過單索引包含多列的方式提高多條件查詢的效率,是數據庫性能優化的重要手段。
最左前綴法則:
聯合索引的列必須從左到右連續使用,否則索引失效。
覆蓋索引:
原理:
通過讓聯合索引覆蓋查詢所需的所有字段,避免回表查詢以提升查詢效率。
-
未覆蓋聯合索引
-
查詢流程
-- 索引(a,b,c,d) SELECT * FROM table WHERE a=1 AND b=2 -- 需回表查詢c,d字段
-
-
覆蓋索引
-
查詢流程
-- 索引(a,b,c,d) SELECT c,d FROM table WHERE a=1 AND b=2 -- 索引中包含查詢的所有字段,無需回表查詢就能得到結果
-
索引有效的查詢:
WHERE a=1 -- 僅用a
WHERE a=1 AND b=2 -- 用a和b
WHERE a=1 AND b=2 AND c=3 -- 用a、b、c
WHERE b=2 AND a=1 -- 使用先后順序不影響有效性
索引失效的各種場景
- 違反最左前綴法則
-- CREATE INDEX idx_abc ON table(a,b,c)
WHERE b=1 -- 跳過最左
WHERE a=1 AND c=2 -- 跳過中間
修復:新建索引。
- 對索引列進行運算或函數操作
-- 索引: (create_time)
SELECT * FROM logs WHERE YEAR(create_time)=2023; -- 索引列函數操作
修復:改為范圍查詢,BETWEEN ’2023-01-01‘ AND ’2023-12-31‘
- 隱式類型轉換
-- 索引: (phone VARCHAR(20))
SELECT * FROM contacts WHERE phone = 13800138000; -- 隱式類型轉換(數字轉字符串)
修復:
WHERE phone = '13800138000' -- 顯式類型匹配
- OR連接非索引列
-- 索引: (status)
SELECT * FROM orders
WHERE status = 'paid' OR amount > 1000; -- ? (amount 無索引)
修復:
-- 方案1: 為 amount 建索引
ALTER TABLE orders ADD INDEX idx_amount (amount);-- 方案2: 改寫為 UNION
SELECT * FROM orders WHERE status = 'paid'
UNION ALL
SELECT * FROM orders WHERE amount > 1000;
- LIKE模糊匹配%通配符在前
-- 索引: (title)
SELECT * FROM articles WHERE title LIKE '%database%';
修復:
-- 使用全文索引
ALTER TABLE articles ADD FULLTEX INDEX(title)SELECT * FROM articles WHERE MATCH(title) AGAINST('database')
- 范圍查詢阻斷后續列
-- 索引: (age, city)
SELECT * FROM users
WHERE age > 20 AND city = 'Beijing'; -- city 失效
修復:
-- 調整索引順序 (范圍查詢置后)
ALTER TABLE users ADD INDEX idx_city_age (city, age);
索引設計原則
- 對數據量大且查詢頻繁的表建立索引。
- 對常用于條件查詢(WHERE)、排序(ORDER BY)、分組(GROUP BY)操作的字段建立索引。
- 字符串類型字段如果字段長度過長,可以建立前綴索引,但要盡量保證區分度。
- 盡量使用聯合索引,聯合索引很多時候可以可以覆蓋索引,避免回表查詢。
- 盡量選擇區分度高的列做索引,區分度越高,回表查詢的次數就越少,檢索效率就越高。
- 索引數量不是越多越好,增加索引數量會影響寫操作的效率。
鎖
鎖是保證數據一致性的并發控制手段。
MySQL中的鎖按照鎖的顆粒度分為三類:
- 全局鎖:鎖定整個數據庫。
- 表級鎖:鎖定整張表。
- 行級鎖:鎖定特定行。
全局鎖
鎖定整個數據庫實例,禁止所有寫操作(DDL,DML),允許讀(DQL)。
鎖定范圍:所有數據庫實例(包括系統數據庫、用戶創建的數據庫)。
用途:數據庫升級或遷移前做全庫的邏輯備份,生成一致性快照。避免邊備份邊寫導致數據庫數據不一致。
flush tables with read lock -- 加全局鎖mysqldump -uroot –p1234 itcast > itcast.sql -- 數據備份unlock tables -- 釋放全局鎖
數據庫加全局鎖存在以下問題:
主庫上加鎖備份,備份期間寫操作不能執行,業務停擺。
從庫備份,備份期間從庫不能執行主庫同步過來的二進制日志(binlog),主從延遲。
在InnoDB引擎中,備份時可已執行以下語句--single-transaction
,不使用全局鎖而獲得一致性快照。
mysqldump --single-transaction -uroot –p123456 itcast > itcast.sql
表級鎖
表鎖
- 讀鎖(共享鎖/S鎖):只能讀表,不能寫操作,可多個事務同時加讀鎖。
- 寫鎖(排它鎖/X鎖):加鎖后其他事務無法再加任何鎖,持鎖事務內可進行讀寫操作。
- 讀鎖升級為寫鎖時該表被其他事務持有讀鎖會阻塞升級,寫鎖與讀鎖互斥。
應用場景:全表更新(UPDATE table SET col=1)。
元數據鎖
-
自動加鎖:訪問表時自動加鎖。
-
鎖定范圍:表結構(列、索引信息)。
-
作用:防止
DDL
與DML
、DQL
語句沖突。-
例如:
-
防止
SELECT
執行期間表結構被修改(ALTER TABLE
) -
防止添加索引(
ALTER table ADD INDEX idx_a_b (a,b)
)期間插入/刪除/更新數據
-
-
意向鎖
核心特性:
- 自動加鎖:當添加行級鎖時由InnoDB自動添加。
- 目的:快速檢查行鎖與表級鎖的兼容性,避免全表掃描判斷。
鎖類型:
- 意向共享鎖(IS):當事務要對表中行加S鎖時自動添加,表示事務準備在表的某些行上加共享鎖。
- 意向排它鎖(IX):當事務要對表中行加X鎖時自動添加,表示事務準備在表的某些行上加排他鎖。
- IS和IX互相不沖突
鎖兼容性:
當前鎖 | 請求IS鎖 | 請求IX鎖 | 請求表S鎖 | 請求表X鎖 |
---|---|---|---|---|
無鎖 | 兼容 | 兼容 | 兼容 | 兼容 |
IS鎖 | 兼容 | 兼容 | 兼容 | 阻塞 |
IX鎖 | 兼容 | 兼容 | 阻塞 | 阻塞 |
表S鎖 | 兼容 | 阻塞 | 兼容 | 阻塞 |
表X鎖 | 阻塞 | 阻塞 | 阻塞 | 阻塞 |
行級鎖
行鎖、間隙鎖、臨鍵鎖是InnoDB實現事務隔離性的關鍵機制,其鎖定粒度最小,發生鎖沖突的概率最低,并發度最高。
行級鎖只在當前讀語句生效,快照讀不觸發。
當前讀的觸發語句:
SELECT ... FOR UPDATE; -- 排他鎖(X鎖)
SELECT ... LOCK IN SHARE MODE; -- 共享鎖(S鎖)
UPDATE ...;
DELETE ...;
INSERT ...;
行鎖(記錄鎖)
-
鎖基于索引實現
InnoDB行鎖通過鎖住索引項實現。若SQL未使用索引,則會鎖住聚簇索引(即主鍵索引)。 -
鎖定的最小化原則
僅鎖定查詢訪問路徑涉及的索引記錄,而非所有關聯索引。例如:- 使用主鍵查詢 → 僅鎖主鍵索引;
- 使用二級索引查詢 → 鎖二級索引+主鍵索引;
- 全表掃描 → 鎖聚簇索引所有行。
-
自動觸發:
-- 對user_id=1的行加排他鎖(X鎖) UPDATE users SET balance = 100 WHERE user_id = 1-- 對user_id=2的行加共享鎖(S鎖) SELECT * FROM users WHERE user_id = 2 LOCK IN SHARE MODE
-
鎖兼容性
當前鎖 請求 S 鎖 請求 X 鎖 S 鎖 允許 阻塞 X 鎖 阻塞 阻塞 -
作用與意義:行鎖主要用于控制對單個數據行的并發訪問,從而支持更高級別的事務隔離,并減少數據操作沖突,提高系統的并發能力,保證數據的一致性和完整性。例如在高并發的電商訂單系統中,對訂單表的某一行記錄加行鎖,可防止多個事務同時修改同一訂單數據,避免數據混亂。
間隙鎖
鎖定索引記錄之間的間隙。
-
目的:防止其他事務在該間隙插入數據,主要用于避免RR隔離級別下的幻讀問題。
-
觸發條件:
-- 當查詢條件無精確匹配時,鎖定間隙 SELECT * FROM users WHERE age > 20 AND age < 30 FOR UPDATE; -- 鎖定(20, 30)的間隙 -- 表中只有10、30的記錄 SELECT * FROM users WHERE age = 20 -- 索引記錄20不存在,鎖定(10,30)的間隙區間
示例場景
假設
users
表有以下數據:id name age 1 Alice 20 2 Bob 30 -- 會話1(事務隔離級別:可重復讀) BEGIN; SELECT * FROM users WHERE age BETWEEN 20 AND 30; -- 返回2行-- 會話2 INSERT INTO users (name, age) VALUES ('Charlie', 25); -- 插入age=25的行-- 會話1再次查詢 SELECT * FROM users WHERE age BETWEEN 20 AND 30; -- 仍返回2行(無幻讀,因間隙鎖)
當會話1執行范圍查詢語句后,會鎖定間隙(20,30)阻塞范圍內的寫操作,直到會話1事務提交。
- 自動觸發:在RR隔離級別下間隙鎖自動觸發,觸發條件包括非唯一索引、范圍查詢、唯一索引等值查詢未命中。
- RR仍然存在的幻讀問題
- 在RR隔離級別下,MySQL提供了兩種機制
- MVCC(快照讀)
- 事務開始時生成一致性視圖。
- 普通
SELECT
語句。 - 讀取事務開始時的數據。
- 鎖機制(當前讀)
SELECT ... FOR UPDATE
- 讀取最新的數據。
- MVCC(快照讀)
- 當事務一開始使用快照讀,快照讀后其他事務進行了寫操作
DELETE
INSERT
,然后當前事務又使用了當前讀,就會出現幻讀現象。
- 在RR隔離級別下,MySQL提供了兩種機制
臨鍵鎖
是InnoDB默認的行鎖算法,本質是行鎖和間隙鎖+間隙鎖的組合。
-
鎖定范圍:鎖定一個左開右閉的區間(a,b]。鎖定的是索引記錄b本身(行鎖),以及該記錄之前的間隙(a,b)(間隙鎖)。
-
幻讀問題場景:
- 事務 A 第一次查詢
WHERE age > 20
,返回 3 條記錄。 - 事務 B 插入一條新記錄
age = 25
并提交。 - 事務 A 再次相同查詢,多出一條記錄(幻影行)。
- 事務 A 第一次查詢
-
臨鍵鎖如何解決:
- 事務 A 執行
SELECT * FROM users WHERE age > 20 FOR UPDATE
時:- 會對
age > 20
的所有索引范圍加臨鍵鎖。 - 例如:若存在
age=18, 22, 30
的記錄,則鎖定的區間包括:(18, 22]
(鎖定age=22
的行 +(18,22)
的間隙)(22, 30]
(鎖定age=30
的行 +(22,30)
的間隙)(30, +∞)
(最后一個記錄之后的間隙鎖)。
- 會對
- 效果:事務 B 無法在
age>20
的任何間隙(如(18,22)
、(22,30)
等)插入新數據(如age=25
),從而徹底杜絕幻讀。
- 事務 A 執行
-
退化行為:
- RU、RC隔離級別:
- 加行鎖
- 不會使用間隙鎖
- 臨鍵鎖退化為行鎖
- 唯一索引未命中時:
- 不加行鎖
- 加間隙鎖(查詢值在索引中應在的索引間隙)
- 臨鍵鎖退化為間隙鎖
- 唯一索引等值查詢命中:
- 加行鎖
- 不加間隙鎖
- 臨鍵鎖退化為行鎖
- 無索引查詢
- 退化為表鎖
- 鎖定所有行和間隙
- RU、RC隔離級別:
FOR UPDATE
(排他鎖)
作用:
對查詢涉及的行(及間隙)加 排他鎖(X 鎖),其他事務無法修改或加鎖這些數據。
行為詳解:
特性 | 說明 |
---|---|
鎖類型 | 排他鎖(X Lock) |
其他事務的讀操作 | 普通 SELECT (快照讀)可讀取,但加鎖讀(如 FOR SHARE )會被阻塞 |
其他事務的寫操作 | 所有寫操作(UPDATE/DELETE/INSERT)都會被阻塞 |
鎖范圍 | 在 REPEATABLE READ 下默認加 臨鍵鎖(Next-Key Lock),鎖住行+間隙 |
使用場景 | 需要修改數據前確保數據不被其他事務修改(如庫存扣減、訂單狀態更新) |
示例:
START TRANSACTION;
-- 對 id=100 的行加排他鎖(其他事務無法修改或加鎖)
SELECT * FROM orders WHERE id = 100 FOR UPDATE;
-- 執行更新操作(此時數據已被鎖定,安全)
UPDATE orders SET status = 'paid' WHERE id = 100;
COMMIT;
FOR SHARE
(共享鎖)
作用:
對查詢涉及的行(及間隙)加 共享鎖(S 鎖),允許其他事務讀,但禁止修改。
行為詳解:
特性 | 說明 |
---|---|
鎖類型 | 共享鎖(S Lock) |
其他事務的讀操作 | 允許其他事務加 FOR SHARE 鎖或普通 SELECT (快照讀) |
其他事務的寫操作 | 禁止其他事務加 FOR UPDATE 或執行寫操作(UPDATE/DELETE/INSERT) |
鎖范圍 | 在 REPEATABLE READ 下也可能加 間隙鎖/臨鍵鎖(防止幻讀) |
使用場景 | 需要讀取數據并確保在事務結束前數據不被修改(如校驗數據一致性) |
示例:
START TRANSACTION;
-- 對用戶余額加共享鎖(其他事務可讀但不可修改)
SELECT balance FROM accounts WHERE user_id = 5 FOR SHARE;
-- 檢查余額是否充足(此時余額不會被其他事務修改)
IF balance >= 100 THENUPDATE accounts SET balance = balance - 100 WHERE user_id = 5;
END IF;
COMMIT;
注意:
MySQL 8.0 之前使用LOCK IN SHARE MODE
,8.0+ 推薦使用標準語法FOR SHARE
。
For UPDATE
在不同隔離級別下的加鎖行為
讀未提交(RU)
加鎖行為
- 行鎖:對查詢命中的索引記錄加排他行鎖,但不加間隙鎖(Gap Lock) 和臨鍵鎖(Next-Key Lock)。
- 無間隙保護:允許其他事務在鎖定記錄的間隙中插入新行,無法防止幻讀。
示例
假設users
表有age
索引,數據為(10, 20, 30)
:
-- 會話1(RU隔離級別)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
-- 命中age=20的記錄,加行鎖
SELECT * FROM users WHERE age = 20 FOR UPDATE;-- 會話2
-- 允許插入間隙(20, 30)中的數據(無間隙鎖)
INSERT INTO users (age) VALUES (25); -- 成功
-- 修改age=20的記錄被阻塞(行鎖生效)
UPDATE users SET name = 'test' WHERE age = 20; -- 阻塞
讀已提交(RC)
加鎖行為
- 行鎖:對查詢命中的索引記錄加排他行鎖。
- 禁用間隙鎖:InnoDB 在 RC 級別會自動關閉間隙鎖,臨鍵鎖退化為行鎖(僅鎖記錄,不鎖間隙)。
示例
-- 會話1(RC隔離級別)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
-- 范圍查詢,命中age=20,加行鎖;但不鎖間隙(10,20)和(20,30)
SELECT * FROM users WHERE age BETWEEN 10 AND 30 FOR UPDATE;-- 會話2
-- 允許插入間隙中的數據(無間隙鎖)
INSERT INTO users (age) VALUES (25); -- 成功
-- 修改age=20的記錄被阻塞(行鎖生效)
UPDATE users SET name = 'test' WHERE age = 20; -- 阻塞
結果
會話 1 再次執行相同查詢時,會讀取到會話 2 插入的age=25
的新行(幻讀發生)。
可重復讀(RR)
加鎖行為
- 行鎖 + 臨鍵鎖 / 間隙鎖:
- 若查詢條件為唯一索引精確匹配(如
WHERE id = 10
,id
是主鍵):僅加行鎖(無間隙鎖)。 - 若查詢條件為非唯一索引或范圍查詢(如
WHERE age > 10
、WHERE age = 20
且age
非唯一):加臨鍵鎖(行鎖 + 間隙鎖),鎖定記錄及前面的間隙。
- 若查詢條件為唯一索引精確匹配(如
- 防止幻讀:通過間隙鎖阻止其他事務在鎖定范圍內插入新行。
唯一索引精確匹配
-- 表結構:id為主鍵(唯一索引),數據為(1, 3, 5)
-- 會話1(RR隔離級別)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
-- 唯一索引精確匹配,僅加行鎖(無間隙鎖)
SELECT * FROM users WHERE id = 3 FOR UPDATE;-- 會話2
-- 允許插入間隙(3,5)中的數據(無間隙鎖)
INSERT INTO users (id) VALUES (4); -- 成功
-- 修改id=3的記錄被阻塞(行鎖生效)
UPDATE users SET name = 'test' WHERE id = 3; -- 阻塞
非唯一索引范圍查詢
-- 表結構:age為非唯一索引,數據為(10, 20, 30)
-- 會話1(RR隔離級別)
BEGIN;
-- 范圍查詢,加臨鍵鎖,鎖定范圍(10, 20]和(20, 30]
SELECT * FROM users WHERE age BETWEEN 10 AND 30 FOR UPDATE;-- 會話2
-- 插入間隙(10,20)中的數據被阻塞(間隙鎖生效)
INSERT INTO users (age) VALUES (15); -- 阻塞
-- 插入間隙(20,30)中的數據被阻塞(間隙鎖生效)
INSERT INTO users (age) VALUES (25); -- 阻塞
-- 修改age=20的記錄被阻塞(行鎖生效)
UPDATE users SET name = 'test' WHERE age = 20; -- 阻塞
串行化(S)
加鎖行為
- 最嚴格鎖定:所有
SELECT ... FOR UPDATE
會鎖定所有訪問的行和間隙,等價于 “行鎖 + 全表間隙鎖”。 - 串行執行:所有事務按順序執行(本質就是嚴格的加鎖行為導致事務只能串行執行),完全禁止并發修改,自然防止臟讀、不可重復讀和幻讀。
- 普通 SELECT 的特殊處理:即使不加
FOR UPDATE/SHARE
,SELECT
也會自動加共享鎖(S 鎖)(隱式轉化為**SELECT ... FOR SHARE
**),阻塞其他事務的寫操作。
示例
-- 會話1(串行化隔離級別)
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- 鎖定age=20的記錄及所有相關間隙
SELECT * FROM users WHERE age = 20 FOR UPDATE;-- 會話2
-- 所有操作均被阻塞(包括查詢和插入)
SELECT * FROM users WHERE age = 20; -- 阻塞(等待共享鎖)
INSERT INTO users (age) VALUES (25); -- 阻塞(間隙被鎖定)
UPDATE users SET name = 'test' WHERE age = 20; -- 阻塞(行鎖生效)
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 記錄(物理日志),以及一個標識事務提交的
日志
事務日志
重做日志Redo Log
InnoDB特有
- 事務持久性保證:確保已提交的事務不會因為數據庫崩潰丟失。
- 崩潰恢復:數據庫崩潰重啟通過Redo 重放在緩沖池中恢復已提交事務修改的數據頁,將數據庫恢復值崩潰前的狀態,然后在一定時機將臟頁持久化到磁盤。
- 性能提升:通過WAL日志先行的操作,修改操作保證日志落盤,不需要立即寫入事務修改的數據頁來保證事務的持久性,從隨機磁盤IO變成了順序磁盤IO。
- 物理特性:記錄的是物理邏輯日志(描述修改操作在哪個表空間哪個頁做了什么修改)
回滾日志Undo Log
InnoDB特有
- 事務原子性保證:撤銷未提交事務的修改,將修改的數據行恢復為Undo Log版本鏈的直接前驅版本,保證事務要么全部成功,要么全部回滾。
- MVCC(多版本并發控制)支持:維護數據行的多個版本,形成undo log版本鏈,配合事務ReadView判斷該事務在該數據行的可見版本,為事務讀操作提供一致性快照,實現非阻塞讀的同時避免了并發事務讀寫沖突。
- 邏輯特性:記錄的是邏輯日志(修改前舊值或恢復修改前狀態所需的信息)
二進制日志Binlog
- 服務層實現:MySQL Server層實現,不涉及具體某個存儲引擎。
- 主從同步:主庫記錄所有更改數據庫的DDL(CREATE、ALTER)和DML(INSERT、UPDATE、DELETE)語句,作為從庫同步數據的來源。
- 數據恢復:可以基于某個時間點的全量備份,加上從該時間點開始的Binlog進行數據恢復,恢復到該時間點之后Binlog有記錄的任意時間點。
錯誤日志Error Log
- 故障診斷:記錄MySQL Server啟動、執行過程中的錯誤信息,警告信息。
- 啟動問題:排查MySQL無法啟動或異常終止的原因。
- 運行錯誤:記錄SQL語句執行錯誤,崩潰堆棧信息等。
慢查詢日志Slow Query Log
- 性能優化:記錄執行超過閾值的SQL語句,幫助開發者找出可能需要優化的SQL。