首頁  >  文章  >  資料庫  >  mysql中RR與幻讀的相關問題

mysql中RR與幻讀的相關問題

WBOY
WBOY轉載
2022-10-11 16:59:021950瀏覽

這篇文章為大家帶來了關於mysql的相關知識,其中主要介紹了關於RR與幻讀的相關內容,包括了MVCC原理、RR產生幻讀、RR解決幻讀等等內容,下面一起來看一下,希望對大家有幫助。

mysql中RR與幻讀的相關問題

推薦學習:mysql影片教學

#一、前言

本文圍繞這三個主題展開學習RR 如何解決幻讀?

mysql中RR與幻讀的相關問題

  • MVCC 原理

  • #實驗:RR 與幻讀

  • 案例:死鎖

先來回顧下MySQL中InnoDB 支援的四個交易隔離和並發事務所帶來的一些問題:

mysql中RR與幻讀的相關問題

  • 讀取未提交:能讀到一個事務的中間過程,違反了ACID 特性,存在髒讀的問題,基本上不會用到。

  • 讀取提交:表示如果其他交易已經提交,那麼就可以看到。在生產環境中用的並不多。

  • 可重複讀取:預設級別,使用最多的一種。其特徵是有 Gap 鎖(間隙鎖)。

  • 可串行化:所有的實作都是透過鎖定來實現的。

並發交易處理也會帶來一些問題:髒讀、不可重複讀取、幻讀

  • 髒讀:一個交易正在對一筆記錄做修改,在這個交易完成並提交前,這條記錄的資料就處於不一致狀態。

  • 無法重複讀取:一個交易以相同查詢條件前後兩次讀取,讀出的資料不一致(修改、刪除)。

  • 幻讀:一個事務內按相同的查詢條件重新查詢數據,卻發現其他事務插入了滿足其查詢條件的新數據。

本文脈絡梳理: RR 為了更快並發,引入 MVCC,但有幻讀的可能,為解決幻讀,引入 Gap 鎖,Gap 可能造成死鎖。

二、MVCC 原理

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 與幻讀

面試題:在RR 事務隔離等級下,事務A查詢一條數據,事務B新增一條數據,事務A能看到事務B的數據嘛?

mysql中RR與幻讀的相關問題

這個問題比較模糊,但大致考察點我們知道是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 &#39;transaction_isolation&#39;; -- 事务隔离级别 RR
select version();                            -- 版本 8.0.16
show variables like &#39;%storage_engine%&#39;;      -- 引擎 InnoDB
-- 1. 手动开启事务提交
begin;  -- 开始事务
commit; -- 提交事务
-- 2. 创建表
CREATE TABLE IF NOT EXISTS `student` (
`id` INT NOT NULL COMMENT &#39;主键 id&#39;,
`name` VARCHAR(50) NOT NULL COMMENT &#39;名字&#39;,
`age` TINYINT NOT NULL COMMENT &#39;年龄&#39;,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT &#39;学生表&#39;;
-- 3. 新增数据用于实验
INSERT INTO student (id, name, age) VALUES (5, &#39;kunkun&#39;, 14);
INSERT INTO student (id, name, age) VALUES (30, &#39;ikun&#39;, 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, &#39;wulikun&#39;, 16);
COMMIT;

發生情況如下圖所示:

mysql中RR與幻讀的相關問題

實驗記錄如下圖所示:

mysql中RR與幻讀的相關問題

現象結論: 當使用目前讀(SELECT ... FOR UPDATE)會產生幻讀。

同樣使用 SELECT ... LOCK IN SHARE MODE; 會產生幻讀。

mysql中RR與幻讀的相關問題

實驗二:先SELECT,再UPDATE

-- 事务A:
BEGIN;
SELECT * FROM student WHERE id < 30;
UPDATE student SET name = &#39;zhiyin&#39; WHERE id = 5;  -- 等待事务B commit 后再执行
SELECT * FROM student WHERE id < 30;
COMMIT;
-- 事务B:
BEGIN;
INSERT INTO student (id, name, age) VALUES (20, &#39;wulikun&#39;, 16);
COMMIT;

發生情況如下圖所示:

mysql中RR與幻讀的相關問題

實驗記錄如下圖:

mysql中RR與幻讀的相關問題

#現象結論: 目前讀(UPDATE)不會產生幻讀。同樣 INSERT / DELETE 皆不會。

mysql中RR與幻讀的相關問題

(2)RR 解決幻讀

實驗如下:

  • #實驗一:快照讀
  • 實驗二:加鎖(更新不存在的記錄)

實驗三:加鎖定(SELECT ... FOR UPDATE)

實驗一:快照讀,普通SELECTmysql中RR與幻讀的相關問題

-- 事务A:
BEGIN;
SELECT * FROM student;
SELECT * FROM student;  -- 等待事务B commit 后再执行
COMMIT;
-- 事务B:
BEGIN;
INSERT INTO student (id, name, age) VALUES (20, &#39;wulikun&#39;, 16);
COMMIT;

發生如下圖所示:

mysql中RR與幻讀的相關問題

實驗記錄如下圖所示:

#現象結論: 在RR 交易隔離層級下,只有快照讀取( SELECT)不會出現幻讀。沒有當前讀。

實驗二:加鎖,(更新不存在的記錄)

######在RR 隔離等級下,交易A 使用UPDATE 加鎖,交易B 無法在這之間插入新數據,這樣事務A在UPDATE 前後讀的數據保持一致,避免了幻讀。 ###
-- 事务A:
BEGIN;
SELECT * FROM student;
UPDATE student SET name = &#39;wulikunkun&#39; WHERE id = 18; -- 记录不存在,产生间隙锁 (5, 30)。
COMMIT;
-- 事务B:
BEGIN;
INSERT INTO student (id, name, age) VALUES (10, &#39;zhiyin&#39;, 16); -- 需要等待事务A结束。
COMMIT;
-- 事务C:
BEGIN;
INSERT INTO student (id, name, age) VALUES (40, &#39;zhiyin你太美&#39;, 32);
COMMIT;
-- 查询数据库中当前有哪些锁
SELECT INDEX_NAME,LOCK_TYPE,LOCK_MODE,LOCK_STATUS,LOCK_DATA FROM performance_schema.data_locks;
###發生情況如下圖所示:###

mysql中RR與幻讀的相關問題

实验记录如下图所示:

mysql中RR與幻讀的相關問題

现象结论:

一开始先加 临键锁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, &#39;zhiyin&#39;, 4); -- 需要等待事务A结束。
COMMIT;
-- 事务C:
BEGIN;
INSERT INTO student (id, name, age) VALUES (5, &#39;zhiyin你太美&#39;, 32); -- 插入成功
COMMIT;
-- 查询数据库中当前有哪些锁
SELECT INDEX_NAME,LOCK_TYPE,LOCK_MODE,LOCK_STATUS,LOCK_DATA FROM performance_schema.data_locks;

发生情况如下图所示:

mysql中RR與幻讀的相關問題

实验记录如下图所示:

mysql中RR與幻讀的相關問題

现象结论:

先加 临键锁Next-key lock,锁范围为 (-∞,5]。

所以,id

拓展:Gap 锁(间隙锁)

根据 官方文档 可知:

  • 锁是加在索引上的。

  • 记录锁: 行锁,只会锁定一条记录。

  • 间隙锁 :是在索引记录之间的间隙上的锁,区间为前开后开 (,)。

  • 临键锁(Next-Key Lock): 由 记录锁 和 间隙锁Gap 组合起来。

  • 加锁的基本单位是 临键锁,其加锁区间为前开后闭 (,]。

  • 索引上的等值查询,给唯一索引加锁的时候,如果满足条件,临键锁 退化为 行锁。

  • 索引上的等值查询,给唯一索引加锁的时候,如果不满足条件,临键锁 退化为 间隙锁。注意,非等值查询是不会优化的。

推荐学习:mysql视频教程

以上是mysql中RR與幻讀的相關問題的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.im。如有侵權,請聯絡admin@php.cn刪除