文章目錄
一、先介紹幾個概念
1、什麼是當前讀
當前讀就是加了鎖的增刪改查語句
2、什麼是快照讀
快照讀讀到的有可能不是數據的最新版本,可能是之前的歷史版本
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上了行鎖,專門應對這種情況。這一部分我就不舉例了,挺詳細的了,可以自己測試一下。
如有問題,請及時指出