這篇文章為大家帶來了關於mysql的相關知識,其中主要介紹了關於RR與幻讀的相關內容,包括了MVCC原理、RR產生幻讀、RR解決幻讀等等內容,下面一起來看一下,希望對大家有幫助。
推薦學習:mysql影片教學
本文圍繞這三個主題展開學習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
拓展:Gap 锁(间隙锁)
根据 官方文档 可知:
锁是加在索引上的。
记录锁: 行锁,只会锁定一条记录。
间隙锁 :是在索引记录之间的间隙上的锁,区间为前开后开 (,)。
临键锁(Next-Key Lock): 由 记录锁 和 间隙锁Gap 组合起来。
加锁的基本单位是 临键锁,其加锁区间为前开后闭 (,]。
索引上的等值查询,给唯一索引加锁的时候,如果满足条件,临键锁 退化为 行锁。
索引上的等值查询,给唯一索引加锁的时候,如果不满足条件,临键锁 退化为 间隙锁。注意,非等值查询是不会优化的。
推荐学习:mysql视频教程
以上是mysql中RR與幻讀的相關問題的詳細內容。更多資訊請關注PHP中文網其他相關文章!