程式設計師必備介面測試偵錯工具:
推薦學習:
本文圍繞這三個話題展開學習 RR 如何解決幻讀?
MVCC 原理
實驗:RR 與 幻讀
案例:死鎖
先來回顧下 MySQL中 InnoDB 支援的四種事務隔離 和 並行事務所帶來的一些問題:
讀未提交:能讀到一個事務的中間過程,違背了 ACID 特性,存在髒讀的問題,基本不會用到。
讀提交:表示如果其他事務已經提交,那麼就可以看到。在生產環境中用的並不多。
可重複讀:預設級別,使用最多的一種。其特點是有 Gap 鎖(間隙鎖)。
可序列化:所有的實現都是通過鎖來實現的。
並行事務處理也會帶來一些問題:髒讀、不可重複讀、幻讀
髒讀:一個事務正在對一條記錄做修改,在這個事務完成並提交前,這條記錄的資料就處於不一致狀態。
不可重複讀:一個事務按相同查詢條件前後兩次讀取,讀出的資料不一致(修改、刪除)。
幻讀:一個事務內按相同的查詢條件重新查詢資料,卻發現其他事務插入了滿足其查詢條件的新資料。
本文脈絡梳理: RR 為了更快並行,引入 MVCC,但有幻讀的可能,為解決幻讀,引入 Gap 鎖,Gap 可能造成死鎖。
MVCC(多版本控制): 指資料庫中為了實現高並行的資料存取,對資料進行多版本處理,並通過事務的可見性來保證事務能看到自己應該看到的資料版本。
MVCC 最大的好處是讀不加鎖,讀寫不衝突。
在 OLTP (On-Line Transaction Processing)應用中,讀寫不衝突很重要,幾乎所有 RDBMS 都支援 MVCC。
注意:MVCC 只在 讀提交RC 和 可重複讀RR 兩種隔離級別下工作。
注意:MVCC 只在 讀提交RC 和 可重複讀RR 兩種隔離級別下工作。
注意:MVCC 只在 讀提交RC 和 可重複讀RR 兩種隔離級別下工作。
(1)MVCC 多版本實現
MySQL 實現 MVCC 機制的時候,是基於 undo log 多版本鏈條 + ReadView 機制。
undo log 多版本鏈: 每一次對資料庫的修改,都會在 undo log 紀錄檔中記錄當前修改記錄的事務號及修改前資料狀態的儲存地址(即 ROLL_PTR),以便在必要的時候可以回滾到老的資料版本。
ReadView 機制: 在多版鏈的基礎上,控制事務讀取的可見性。(主要區別是:RC 和 RR)
這裡不著重探究原理,但要有大概的概念:undo log 多版本鏈 和 ReadView 機制。
針對 undo log 多版本鏈,舉個栗子:
一個讀事務查詢到當前記錄,而最新的事務還未提交。
根據原子性,讀事務看不到最新資料,但可以去回滾段中找到老版本的資料,這樣就生成了多個版本。
針對 ReadView 機制: 基於 undo log 多版本鏈實現,不同事務隔離有不同處理 :
RC 級別的事務: 可見性比較高,它可以看到已提交的事務的所有修改。
RR 級別的事務: 一個讀事務中,不管其他事務對這些資料做了什麼修改,以及是否提交,只要自己不提交,查詢的資料結果就不會變。
這是如何做到的呢?
RC讀提交: 每一條讀操作語句都會獲取一次 ReadView,每次更新之後,都會獲取資料庫中最新的事務提交狀態,也就可以看到最新提交的事務了,即每條語句執行都會更新其可見性檢視。
RR可重複讀: 開啟事務時不會獲取 ReadView,只有發起第一個快照讀時才會獲取 ReadView。
如果使用當前讀,都會獲取新的 ReadView,也能看到更新的資料。
(2)快照讀與當前讀
在 MVCC 並行控制中,讀操作 可以分為兩類:
快照讀:讀取的是記錄的可見版本(有可能是歷史版本), 不用加鎖 。
操作:簡單的 SELECT 操作。
當前讀:讀取的是記錄的最新版本,並且當前讀返回的記錄,都會加鎖,保證其他事務不會再並行修改這條記錄。
操作:特殊讀操作、新增/更新/刪除操作。
-- 對應 SQL 如下:
-- 1. 特殊讀操作
SELECT ... FOR UPDATE
SELECT ... LOCK IN SHARE MODE -- 共用鎖
-- 2. 新增:INSERT
-- 3. 更新:UPDATE
-- 4. 刪除:DELETE
登入後複製
結合 ReadView 機制來區分:快照讀 和 當前讀:
快照讀: 在一個事務裡,只有發起第一個快照讀時才會獲取 ReadView,之後的讀操作不會再獲取。
當前讀: 每次讀操作都會獲取 ReadView。
面試題:在 RR 事務隔離級別下,事務A查詢一條資料,事務B新增一條資料,事務A能看到事務B的資料嘛?
這個問題比較模糊,但大致考察點我們知曉是 RR 與 幻讀,可以將問題分為兩類:
什麼情況下,RR 產生幻讀?(能看到資料)
答案:當前讀(SELECT..FOR UDPDATE、SELECT ... LOCK IN SHARE MODE)
什麼情況下,RR 解決幻讀?(不能看到資料)
答案:加鎖、快照讀
注意: 不可重複讀 重點在於 UPDATA 和 DELETE,而幻讀的重點在於 INSERT。
它們之間最大的區別:是如何通過鎖機制來解決它們產生的問題。
這裡說的鎖只是使用悲觀鎖機制。
再來回顧下:幻讀
-- 舉個栗子:有這樣一個查詢 SQL
SELECT * FROM user WHERE id < 10;
登入後複製
在同一個事務下,T1時刻查詢出來 4 條資料,T2時刻查詢出來 8 條資料。這就產生了幻讀。
在同一個事務下,T1時刻查詢出來 8 條資料,T2時刻查詢出來 4 條資料。這就產生了幻讀。
實驗準備如下: 動手實踐起來
show variables like 'transaction_isolation'; -- 事務隔離級別 RR
select version(); -- 版本 8.0.16
show variables like '%storage_engine%'; -- 引擎 InnoDB
-- 1. 手動開啟事務提交
begin; -- 開始事務
commit; -- 提交事務
-- 2. 建立表
CREATE TABLE IF NOT EXISTS `student` (
`id` INT NOT NULL COMMENT '主鍵 id',
`name` VARCHAR(50) NOT NULL COMMENT '名字',
`age` TINYINT NOT NULL COMMENT '年齡',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT '學生表';
-- 3. 新增資料用於實驗
INSERT INTO student (id, name, age) VALUES (5, 'kunkun', 14);
INSERT INTO student (id, name, age) VALUES (30, 'ikun', 18);
登入後複製
(1)RR 產生幻讀
實驗如下: 測試當前讀
實驗一:先 SELECT,再 SELECT ... FOR UPDATE
實驗二:先 SELECT,再 UPDATE (不會產生幻讀)
實驗一:先 SELECT,再 SELECT ... FOR UPDATE
-- 事務A:
BEGIN;
SELECT * FROM student WHERE id < 30;
SELECT * FROM student WHERE id < 30 FOR UPDATE; -- 等待事務B commit 後再執行
-- SELECT * FROM student WHERE id < 30 LOCK IN SHARE MODE;
COMMIT;
-- 事務B:
BEGIN;
INSERT INTO student (id, name, age) VALUES (20, 'wulikun', 16);
COMMIT;
登入後複製
發生情況如下圖所示:
實驗記錄如下圖所示:
現象結論: 當使用當前讀(SELECT ... FOR UPDATE)會產生幻讀。
同樣使用 SELECT ... LOCK IN SHARE MODE; 會產生幻讀。
實驗二:先 SELECT,再 UPDATE
-- 事務A:
BEGIN;
SELECT * FROM student WHERE id < 30;
UPDATE student SET name = 'zhiyin' WHERE id = 5; -- 等待事務B commit 後再執行
SELECT * FROM student WHERE id < 30;
COMMIT;
-- 事務B:
BEGIN;
INSERT INTO student (id, name, age) VALUES (20, 'wulikun', 16);
COMMIT;
登入後複製
發生情況如下圖所示:
實驗記錄如下圖所示:
現象結論: 當前讀(UPDATE)不會產生幻讀。同樣 INSERT / DELETE 均不會。
(2)RR 解決幻讀
實驗如下:
實驗一:快照讀
實驗二:加鎖(更新不存在的記錄)
實驗三:加鎖(SELECT ... FOR UPDATE)
實驗一:快照讀,普通 SELECT
-- 事務A:
BEGIN;
SELECT * FROM student;
SELECT * FROM student; -- 等待事務B commit 後再執行
COMMIT;
-- 事務B:
BEGIN;
INSERT INTO student (id, name, age) VALUES (20, 'wulikun', 16);
COMMIT;
登入後複製
發生情況如下圖所示:
實驗記錄如下圖所示:
現象結論: 在 RR 事務隔離級別下,只有快照讀(SELECT)不會出現幻讀。沒有當前讀。
實驗二:加鎖 ,(更新不存在的記錄)
在 RR 隔離級別下,事務 A 使用 UPDATE 加鎖,事務 B 無法在這之間插入新資料,這樣事務 A在 UPDATE 前後讀的資料保持一致,避免了幻讀。
-- 事務A:
BEGIN;
SELECT * FROM student;
UPDATE student SET name = 'wulikunkun' WHERE id = 18; -- 記錄不存在,產生間隙鎖 (5, 30)。
COMMIT;
-- 事務B:
BEGIN;
INSERT INTO student (id, name, age) VALUES (10, 'zhiyin', 16); -- 需要等待事務A結束。
COMMIT;
-- 事務C:
BEGIN;
INSERT INTO student (id, name, age) VALUES (40, 'zhiyin你太美', 32);
COMMIT;
-- 查詢資料庫中當前有哪些鎖
SELECT INDEX_NAME,LOCK_TYPE,LOCK_MODE,LOCK_STATUS,LOCK_DATA FROM performance_schema.data_locks;
登入後複製
發生情況如下圖所示:
實驗記錄如下圖所示:
現象結論:
一開始先加 臨鍵鎖Next-key lock,鎖範圍為 (5,30]。
因為是唯一索引,且更新的記錄不存在,臨鍵鎖退化成 間隙鎖Gap,最終鎖範圍為 (5,30)。其餘的記錄不受影響。
實驗三:加鎖(SELECT ... FOR UPDATE)
-- 事務A:
BEGIN;
SELECT * FROM student;
SELECT * FROM student WHERE id < 5 FOR UPDATE;
COMMIT;
-- 事務B:
BEGIN;
INSERT INTO student (id, name, age) VALUES (4, 'zhiyin', 4); -- 需要等待事務A結束。
COMMIT;
-- 事務C:
BEGIN;
INSERT INTO student (id, name, age) VALUES (5, 'zhiyin你太美', 32); -- 插入成功
COMMIT;
-- 查詢資料庫中當前有哪些鎖
SELECT INDEX_NAME,LOCK_TYPE,LOCK_MODE,LOCK_STATUS,LOCK_DATA FROM performance_schema.data_locks;
登入後複製
發生情況如下圖所示:
實驗記錄如下圖所示:
現象結論:
先加 臨鍵鎖Next-key lock,鎖範圍為 (-∞,5]。
所以,id < 5 和 id = 5 的資料都插入不進去。
拓展:Gap 鎖(間隙鎖)
根據 官方檔案 可知:
鎖是加在索引上的。
記錄鎖: 行鎖,只會鎖定一條記錄。
間隙鎖 :是在索引記錄之間的間隙上的鎖,區間為前開後開 (,)。
臨鍵鎖(Next-Key Lock): 由 記錄鎖 和 間隙鎖Gap 組合起來。
加鎖的基本單位是 臨鍵鎖,其加鎖區間為前開後閉 (,]。
索引上的等值查詢,給唯一索引加鎖的時候,如果滿足條件,臨鍵鎖 退化為 行鎖。
索引上的等值查詢,給唯一索引加鎖的時候,如果不滿足條件,臨鍵鎖 退化為 間隙鎖。注意,非等值查詢是不會優化的。
推薦學習:
以上就是mysql中RR與幻讀的相關問題的詳細內容,更多請關注TW511.COM其它相關文章!