文章目錄
- 復習 MySQL Day1:事務
- MySQL 事務的四大特性?
- 并發事務會出現什么問題?
- MySQL 事務的隔離級別?
- 不同事務隔離級別下會發生什么問題?
- MVCC 的實現原理?
- 核心數據結構
- 版本鏈構建示例
- 可見性判斷算法
- MVCC 可見性判斷總結
- 幻讀如何解決?
- 讀已提交隔離級別如何實現?
復習 MySQL Day1:事務
MySQL 事務的四大特性?
- 原子性:事務當中的若干條數據庫操作要么全部成功,要么全部失敗;
- 一致性:數據庫總是從一個一致性的狀態遷移到另一個一致性的狀態;
- 隔離性:事務提交之前,對數據庫做出的更改對其他事務不可見;
- 持久性:事務提交之后就會被持久化到磁盤當中。
并發事務會出現什么問題?
指的主要是由 MySQL 隔離級別的不同帶來的若干問題。
- 臟讀:事務還未提交,其所做的修改就已經可以被其他事務所讀取了;
- 不可重復讀:事務開始時讀取的數據與事務執行過程中讀取到的數據不同;
- 幻讀:通常發生在區間查詢的場景下,指的是在一個事務執行期間內,上一次范圍查詢讀取到的數據與本次范圍查詢讀取到的數據不一致,其發生的原因可能是在事務執行期間有其他事務向第一次查詢的數據區間當中插入了新的數據。
MySQL 事務的隔離級別?
- 讀未提交:事務還未提交,其他事務就可看到其所做的更改,隔離級別最低;
- 讀已提交:事務提交之后,其所做的更改才能夠被其他事務所看到。在讀已提交隔離級別下,當前事務可以看到其他事務執行完成后所做的修改;
- 可重復讀:MySQL InnoDB 數據引擎的默認隔離級別,指的是從事務開始到事務結束期間,讀取到的數據都是一致的;
- 串行化:對記錄加上讀寫鎖,在多個事務進行讀寫的過程中,如果發生了鎖沖突,那么當前事務必須等上一個事務讀寫完成方可進行讀寫。串行化不存在并發事務的問題,但它的并發性能最差。
不同事務隔離級別下會發生什么問題?
- 讀未提交:臟讀、不可重復讀、幻讀;
- 讀已提交:不可重復讀、幻讀;
- 可重復讀:幻讀;
- 串行化:不存在并發事務的問題;
MVCC 的實現原理?
MVCC 是數據庫實現并發控制的關鍵技術,InnoDB 數據引擎通過 MVCC 實現讀操作的并發,極大提高了數據庫的并發性能。
核心數據結構
隱藏字段
InnoDB 為每行記錄添加了以下隱藏字段:
- DB_TRX_ID:記錄創建或最后一次修改該行記錄的事務 ID;
- DB_ROLL_PTR:回滾指針,指向 undo log 記錄;
- DB_ROW_ID:隱含的自增行 ID;
- DELETE BIT:記錄該行是否刪除。
Undo Log(回滾日志)
- 存儲行記錄的歷史版本;
- 組成版本鏈,通過 DB_ROLL_PTR 指針連接;
- 用于事務回滾與 MVCC 讀取。
Read View(讀視圖)
- m_ids:生成 Read View 時活躍的事務 ID 列表;
- min_trx_id:m_ids 中的最小事務 ID;
- max_trx_id:m_ids 中的最大事務 ID;
- create_trx_id:創建該 Read View 的事務 ID。
版本鏈構建示例
- 每次更新操作都會在 undo log 記錄舊數據版本;
- 通過 DB_ROLL_PTR 指針形成單向鏈表;
- 鏈表頭是最新版本,尾部是最舊版本。
可見性判斷算法
比較 DB_TRX_ID 與 creator_trx_id
如果相等,說明當前記錄是該事務自身修改的事務,對當前事務可見。
檢查 DB_TRX_ID < min_trx_id
說明這條記錄在當前 Read View 生成之前已經提交,對當前事務可見。
檢查 DB_TRX_ID >= max_trx_id
說明這條記錄是在 Read View 生成之后創建的,對當前事務不可見。
檢查 DB_TRX_ID 是否在 m_ids 事務活躍列表當中
- 存在:生成 Read View 時當前記錄仍活躍,對當前事務不可見;
- 不存在:最后一次修改該條記錄的事務已提交,對當前事務可見;
MVCC 可見性判斷總結
總的來說,針對基于 MVCC 的事務可見性判斷,關鍵的字段包括以下幾個:
- 對于記錄,每一條記錄都有一個隱式的 DB_TRX_ID 字段,用于記錄最后一個修改這條記錄的事務 ID;
- 對于 SELECT 操作生成的 Read View,其隱式包含以下幾個字段:
1)m_ids:Read View 生成時活躍的事務 ID 列表;
2)min_trx_id:m_ids 中最小的事務 ID;
3)max_trx_id:m_ids 中最大的事務 ID;
4)create_trx_id:創建這個 Read View 的事務 ID。
可見性判斷的算法流程如下:
- 首先對比記錄的 DB_TRX_ID 和 creator_trx_id,相等則代表該記錄最后一次由當前視圖修改,對該事務可見;
- 之后再比對 DB_TRX_ID 和 min_trx_id 以及 max_trx_id 的大小,如果小于 min_trx_id,說明修改該記錄的事務在生成 Read View 時已提交,對當前事務可見;如果大于 max_trx_id,說明修改該記錄的事務在當前事務之后創建,其所做的修改對當前事務不可見,通過 DB_ROLL_PTR 找到該記錄的上一個版本。
- 最后查看 DB_TRX_ID 是否在 m_ids 當中,如果在,說明修改這條記錄的事務在活躍列表當中,該記錄的當前版本對當前事務不可見;否則說明修改該記錄的事務已經提交,這條記錄對當前事務可見。
幻讀如何解決?
通過快照讀(一致性非鎖定讀)
對于普通的 SELECT 查詢語句,InnoDB 使用 MVCC 提供一致性視圖,避免看到其他事務插入的數據。
使用間隙鎖(Gap Lock)和臨鍵鎖(Next-Key Lock)
InnoDB 在可重復讀隔離級別下通過以下方式防止幻讀:
- 間隙鎖(Gap Lock):鎖定索引記錄之間的間隙;
- 臨鍵鎖(Next-Key Lock):臨鍵鎖是記錄鎖(行鎖)+ 間隙鎖的組合,鎖定記錄及其前面的間隙。
一個基于間隙鎖 + 臨鍵鎖防止幻讀的例子如下:
-- 事務1
BEGIN;
SELECT * FROM users WHERE age > 20 FOR UPDATE; -- 鎖定age>20的所有記錄和間隙
-- ?? 顯式地使用區間鎖鎖定一個范圍, 避免在范圍內有新記錄插入
-- 此時事務2嘗試插入age>20的記錄會被阻塞
INSERT INTO users(name, age) VALUES('new_user', 25); -- 阻塞
總結
使用「MVCC 版本控制」或「間隙鎖 + 臨鍵鎖」這兩種方式可以避免幻讀的問題。
讀已提交隔離級別如何實現?
在讀已提交隔離級別下,每次執行 SELECT 語句都會創建一個 Read View。創建 Read View 時已經提交的事務所做的修改對當前事務是可見的(會導致不可重復讀問題),但未提交以及當前事務之后的事務所做的修改不可見。