數據庫之InnoDB可重複讀隔離級別下如何避免幻讀

一、先介紹幾個概念

1、什麼是當前讀

什麼是快照讀和當前讀
當前讀就是加了鎖的增刪改查語句

2、什麼是快照讀

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-VBOB0mpv-1587714794974)(C:\Users\Taogege\AppData\Roaming\Typora\typora-user-images\image-20200422190432323.png)]
快照讀讀到的有可能不是數據的最新版本,可能是之前的歷史版本

3、什麼是mvcc

mvcc全稱是multi version concurrent control(多版本併發控制)。mysql把每個操作都定義成一個事務,每開啓一個事務,系統的事務版本號自動遞增。每行記錄都有兩個隱藏列:創建版本號和刪除版本號

二、RR級別下避免幻讀的方法

  • 表象:快照讀(非阻塞讀)–InnoDB實現了僞MVCC來避免幻讀。
  • 內在:next-key鎖(X鎖+gap鎖),當前讀情況下通過next-key鎖避免幻讀

三、RC級別下測試快照讀和當前讀

創建表如下(account_innodb):
在這裏插入圖片描述
添加數據如下
在這裏插入圖片描述
開啓兩個查詢,會話1和會話2,兩個都取消自動提交:set autocommit = 0;,都開啓事務:start TRANSACTION;

3.1、測試快照讀

會話1開啓事務後執行以下sql:

select * from account_innodb where id = 2;

在這裏插入圖片描述
然後會話2對該記錄進行更新並提交

update account_innodb set balance = 700 where id = 2;
COMMIT;

然後再用會話1查詢得出:
在這裏插入圖片描述

3.2、測試當前讀

與快照讀有一點不同的是select加上了共享鎖,緊接着上邊的操作,快照讀查詢完後,用當前讀查詢一遍:

select * from account_innodb where id = 2 lock in share mode;

在這裏插入圖片描述
可見RC下的快照讀和當前讀查詢結果是一樣的

四、RR級別下測試快照讀和當前讀

操作與上邊差不多,新建兩個查詢,自動提交關閉掉,將隔離級別調整爲repeatable read,mysql默認的級別就是這個,然後開啓事務:start TRANSACTION;
會話1

select * from account_innodb where id = 2;

結果爲:
在這裏插入圖片描述
然後我們會話2來更新數據:select * from account_innodb where id = 2;並提交事務,然後會到會話1用快照讀
在這裏插入圖片描述
發現是原數據,我們再用當前讀:select * from account_innodb where id = 2 lock in share mode;
在這裏插入圖片描述
發現是剛剛更新的數據

那RR級別下的快照讀能讀到最新的數據嗎?肯定可以

還是上邊的基礎,即RR級別,先關閉掉兩個會話的事務,重新開啓事務,直接執行會話2

update account_innodb set balance = 900 where id = 2;
commit;

然後回到會話1,執行快照讀:select * from account_innodb where id = 2;,得到結果:
在這裏插入圖片描述
是更新後的數據,再執行以下當前讀:select * from account_innodb where id = 2 lock in share mode;
在這裏插入圖片描述
結果一樣,到了這一步不難想到,RR級別下的快照讀跟他什麼時候執行有關,先這麼說,後邊詳細解釋

五、RC、RR級別下的InnoDB的非阻塞讀(快照讀)如何實現主要

主要靠下邊三點:
在這裏插入圖片描述

一個事務的情況下的undo log存儲情況
在這裏插入圖片描述

第一行右邊的三個字段是關鍵:

  • DB_ROW_ID(標識插入的新的數據行的id)
  • DB_TRX_ID(事務ID
  • DB_ROLL_PTR(回滾指針)指向Undo log中的數據行

可以看到在Field2的數據進行更新之前,數據庫會將更新之前的數據複製進update(包含update/delete) Undo log中,快照讀通過DB_ROLL_PTR指針讀取之前的數據行,這樣一來,快照讀每次讀的就是更新前的穩定數據,可以用來表象上避免幻讀,有的朋友可能會問,RC級別爲什麼不能避免,我們後邊會說。

undo log是幹什麼的

undo日誌用於存放數據修改被修改前的值,假設修改 tba 表中 id=2的行數據,把Name=‘B’ 修改爲Name = ‘B2’ ,那麼undo日誌就會用來存放Name='B’的記錄,如果這個修改出現異常,可以使用undo日誌來實現回滾操作,保證事務的一致性。

read view呢

  • Read View就是事務進行快照讀操作的時候生產的讀視圖(Read View),在該事務執行的快照讀的那一刻,會生成數據庫系統當前的一個快照,記錄並維護系統當前活躍事務的ID(當每個事務開啓時,都會被分配一個ID, 這個ID是遞增的,所以最新的事務,ID值越大)
  • 它決定的是當前事務能看到的是哪個事務操作前的數據。它遵循一個可見性算法:將要修改的數據事務的DB_TRX_ID取出來與系統其它事務活躍ID做對比,如果大於或者等於這些ID,那麼就取出當前DB_TRX_ID的事務的DB_ROLL_PTR指針所指向的undo log的版本數據,直到DB_TRX_ID小於這些系統活躍事務的ID,這樣即可取到穩定的數據。注意(每當進入一個transaction時,事務ID就會增大)。
    示意圖如下圖——多個事務操作,undo log 數據存儲圖:
    在這裏插入圖片描述
    多個事務操作就會有數據的多個版本,如上圖,第二行在被第二個事務修改前要將數據拷貝到Undo log中並有指針指向。可以說DB_ROLL_PTR是連接undo版本數據的關鍵。

下面解釋一下前邊的問題:爲什麼RC、RR級別下的快照讀有區別

在RC級別下,快照讀每調用一次,那麼以上的Undo log操作就會執行一次,裏面的版本數據就會更新到最新。而在RR級別下,我們第一次調用快照讀,創建了Undo log,後邊是不會再執行,直到提交事務,譬如我們上邊的RR測試,先執行一次快照讀,再更新一次,然後再讀,還是之前的數據,有心的朋友可以繼續更新一次提交,然後回到會話1再進行一次快照讀,還是最開始的數據,從而達到避免幻讀,所以對於RR級別來說,其第一次快照讀的時機很重要。這也是爲什麼RC不可以避免幻讀的原因。

以上都是表象部分,只不過是先進行增刪改事務,導致read view的能獲取到的是可見性版本內的數據,InnoDB的RR及以上級別避免幻讀的內在是next-key鎖。還有SERIALIZABLE隔離級別的快照讀可不像其他級別那樣無阻塞,這裏的快照讀是要上共享鎖的,所以下面還是要說一下next-key(行鎖+gap鎖)

六、next-key(行鎖+gap鎖)

行鎖我們都知道,那麼gap鎖是什麼?

gap鎖是間隙鎖,即鎖定一個範圍,但不包括記錄本身,它的目的是爲了防止事務的兩次當前讀產生幻讀
gap鎖上鎖條件:

  • 如果where條件全部命中,則不會用Gap鎖,只會加記錄鎖
  • 如果where條件部分命中或者全不命中,則會加Gap鎖
  • 如果sql走的是非唯一索引或者不走索引,則會加Gap鎖

下邊我們進行測試
建立表tb:


SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for tb
-- ----------------------------
DROP TABLE IF EXISTS `tb`;
CREATE TABLE `tb` (
  `name` varchar(10) NOT NULL,
  `id` int(100) NOT NULL DEFAULT '0',
  PRIMARY KEY (`name`),
  UNIQUE KEY `unique_id` (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

(數據自擬),我的是:
在這裏插入圖片描述
創建兩個session,隔離級別默認RR,都開啓事務

6.1、測試sql走唯一索引,並精準命中

session1:

# 檢測是否走的唯一鍵
explain delete from tb where id = 8; 

在這裏插入圖片描述
然後我們測試執行該sql,然後在其前後插入數據,看有沒有間隙鎖存在
session1:

delete from tb where id = 8;

session2:

insert into tb values('ii',9);

在這裏插入圖片描述
插入成功,說明沒有gap鎖

6.2、測試部分命中、精準命中(不存在的值)、精確命中全部數據

session1和session2 rollback,然後開啓事務

  • 先測試不存在的記錄:
    session1
delete from tb where id = 13;

session2

insert into tb values('i',14);
# 結果被阻塞

說明不存在的值,等於沒有被命中,要上Gap鎖

  • 部分命中測試
    爲了方便測試我們刪除數據id=6
    session1
select * from tb where id in (5,6,8) lock in share mode;

在這裏插入圖片描述
session2:

insert into tb values('i',4);
# 插入成功
insert into tb values('ii',7);
# 7插入失敗,被阻塞
insert into tb values('ii',6);
# 6插入失敗,被阻塞
insert into tb values('ii',6);
# 9插入成功

總結:部分命中的話,也是中間部分加鎖

  • 精確命中全部數據測試

回滾之前事務,並開啓事務
session1

select * from tb where id in (5,6,9) lock in share mode;

在這裏插入圖片描述
session2

insert into tb values('iii',7);
# 插入成功
insert into tb values('iii',8);
# 插入成功

總結:全部命中不會上Gap鎖

驗證了前面的兩點總結

6.3、Gap鎖用在非唯一索引或者不走索引的當前讀

圖示
在這裏插入圖片描述
我們可以看到,上圖下邊顯示的是間隙,真正上Gap鎖的部分是(6,9]和(9,11],遵循左開右閉原則,凡是插入的id在區間之中都被阻塞,還有邊界情況,比如(6,a)是可以插入的,因爲a的ASCII碼在c的前面,(6,dd)是不可以插入的,道理一樣,(11,h)可以插入,(11,a)就不行,可能有人會說(9,c)不在範圍裏面啊,但是也不行,因爲id=9上了行鎖,專門應對這種情況。這一部分我就不舉例了,挺詳細的了,可以自己測試一下。

如有問題,請及時指出

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章