MySQL事務隔離級別及鎖的試驗

一.事務ACID

在這裏插入圖片描述

二.MySQL四種隔離級別

隔離級別 髒讀 不可重複讀 幻讀
Read uncommitted(讀未提交)
Read committed(讀已提交)
Repeatable read(可重複讀)
Serializable(串行化)

事務的隔離級別基本是爲了解決讀一致性的問題。
下面我會通過一個經典的銀行賬戶餘額變更的例子來依次闡述。

三.環境搭建

1.表創建

創建表account,並插入兩條數據。使用navicat打開

CREATE TABLE `account` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(10) DEFAULT NULL,
  `account` double DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

在這裏插入圖片描述

2.關閉自動提交

SHOW VARIABLES LIKE 'AUTOCOMMIT';
SET AUTOCOMMIT = 'off';

在這裏插入圖片描述
MYSQL默認是自動提交事務的,其實不關閉也沒問題,因爲我們會使用SQL語句顯式的開啓一個事務。

3.打開兩個會話

我分別使用navicat和sqlyog來表示兩個客戶端的請求。
在這裏插入圖片描述
在這裏插入圖片描述

四.讀未提交

1.分別設置Mysql的會話級別隔離級別爲讀未提交

set session transaction isolation level read uncommitted;

在navicat及sqlyog的會話窗口先執行此sql,修改事務的隔離級別爲讀未提交。
以下navicat的會話sql當做A,sqlyog的會話當做B。

2.A中開啓事務並執行更新操作

BEGIN;
update account set account = account + 200 where id = 1;

注意此時A事務並沒有提交!

3.B中開啓事務,並執行查詢

BEGIN;
SELECT * FROM account;

在這裏插入圖片描述
我們看到在B事務中,張三的賬戶餘額變爲了1200。也就是說我們讀到了A事務沒有提交的數據,也就是說發生了髒讀。假設此時我們將A事務回滾(執行CALLBACK),張三的賬戶餘額會變回1000,那麼B讀取到的數據1200就是髒數據。

4.總結

讀未提交是最低的隔離級別,沒有解決任何一個事務併發問題。發生髒讀時B事務並不知道A事務是否提交會回滾,所以拿到的數據是很不安全的。

五.讀已提交

1.分別設置Mysql的會話級別隔離級別爲讀已提交

set session transaction isolation level read committed;

在navicat及sqlyog的會話窗口先執行此sql,修改事務的隔離級別爲讀已提交。

2.A中開啓事務並執行更新操作

BEGIN;
update account set account = account + 200 where id = 1;

注意此時A事務並沒有提交!

3.B中開啓事務,並執行查詢

BEGIN;
SELECT * FROM account;

在這裏插入圖片描述
我們可以看到此時B事務並沒有讀取到A事務已變化的內容,我們並沒有看到張三的account由1000變爲1200。這一切似乎已經很完美了,解決了髒讀的問題,我們已經讀取不到沒提交的事務所修改的數據了。

但是請注意:
當A事務提交後,B事務再次查詢會看到account變爲了1200。也就是說B事務兩次查詢的結果是不一致的,此時發生了不可重複讀現象不可重複讀的意思就是在一個事務中相同的查詢條件下卻讀取到不同的結果。

此時我們需要將mysql的隔離級別提高到可重複讀
Repeatable read(可重複讀),同時也是MySQL InnoDB引擎的默認事務隔離級別。

4.總結

雖然讀已提交解決了髒讀的問題,但是不可重複讀的問題卻沒能解決,此時需要提高隔離級別到Repeatable read(可重複讀)

六.可重複讀

1.分別設置Mysql的會話級別隔離級別爲可重複讀

set session transaction isolation level repeatable read;

在navicat及sqlyog的會話窗口先執行此sql,修改事務的隔離級別爲可重複讀。簡稱RR隔離級別。

2.A中開啓事務並執行更新操作

BEGIN;
update account set account = account + 200 where id = 1;

注意此時A事務並沒有提交!

3.B中開啓事務,並執行查詢

BEGIN;
SELECT * FROM account;

在這裏插入圖片描述
可以看到此時我們讀取的是A事務沒變更前的數據,即張三的account還是1000,首先解決了髒讀的問題。其次我們對A事務進行提交。

在B事務中(B還未提交)再次查詢結果。在這裏插入圖片描述
發現B事務讀取的仍然是1000。說明即便A事務提交了記錄,B事務多次讀取依舊能夠讀取到相同的結果,解決了不可重複讀的問題。

此時我們提交B事務。再次執行查詢
在這裏插入圖片描述
可以看到在A和B事務都提交後,可以查詢出A事務中執行的修改內容,此時張三的account變爲了1200。

4.總結

Repeatable read(可重複讀)解決了髒讀,不可重複讀的問題,是MySQL的InnoDB引擎默認的事務隔離級別,可以解決大部分的讀一致性問題。
但是Repeatable read(可重複讀)還有一個問題沒能解決,那就是幻讀。
幻讀和不可重複讀容易混淆,在我理解看來。

  • 不可重複讀主要針對update的操作,表現爲一個事務多次讀取另一個事務中修改後的內容,讀取到的結果不一致,大部分是值的變化(舉個例子,金額)。
  • 幻讀可能在insert,delete中發生比較頻繁。比如A事務中進行了insert或者delete操作,導致B事務在原先的範圍條件讀取時,結果變多了或者變少了。(舉個例子,在A事務中添加一條account記錄,id爲3,username爲王五。B事務使用where id > 1查詢,原先可能查詢只有一條記錄,id=2的李四記錄。但此時卻查詢到了兩條記錄,即id = 3的記錄,就像是發生了幻覺一樣,也就是幻讀了。)

七.串行化

set session transaction isolation level serializable;

串行化是事務隔離級別中最高的一級,可以解決髒讀、不可重複讀、幻讀的問題。但同時也是效率最低、併發度最差的一種,原因是串行化每次讀寫操作都會鎖表,必須等待鎖釋放下一個事務才能進行相關的操作。所以一般很少用到,這裏不再試驗,下面將對幻讀進行測試。

八.幻讀測試1

在Innodb的Repeatable read(可重複讀)隔離級別下,幻讀的問題其實已經被InnoDB解決了。所以按照下面的測試步驟,是看不到幻讀的現象的。

1.在A中執行

set session transaction isolation level repeatable read;

BEGIN;
#插入一條id爲3的記錄
insert into account value(3,'王五',1000);

2.在B中執行

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

BEGIN;
SELECT * FROM account WHERE id > 1;

3.A事務沒提交的查詢結果

在這裏插入圖片描述
如圖,在A事務沒提交的情況下,我們看到只查到一條數據,這也很合理。那麼接下來我們提交A事務呢?

4.A事務提交的查詢結果

提交A事務

COMMIT;

同時在B事務中再次查詢

SELECT * FROM account WHERE id > 1;

在這裏插入圖片描述
結果仍然一樣。

5.B事務提交再次查詢

在這裏插入圖片描述
此時終於看到了A事務中插入的記錄。
這麼看來,我們所說的幻讀現象並沒有出現,那麼怎麼才能測試出來呢?

九.幻讀測試2

1.A中執行測試1中相同的內容

set session transaction isolation level repeatable read;

BEGIN;
#插入一條id爲3,姓名爲王五的記錄
insert into account value(3,'王五',1000);

2.B中執行插入操作

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

BEGIN;
#插入一條id爲3,姓名爲趙六的記錄
INSERT INTO account VALUE(3,'趙六',1000);

在這裏插入圖片描述
會發現B事務並不能插入,一直處於操作狀態。等一小段時間過後,我們會在B事務的控制檯看到報錯信息
在這裏插入圖片描述
這說明我們當前的兩個事務中存在着鎖的衝突,爲什麼會這樣呢?
因爲我們操作A事務,執行insert id=3的操作,B事務同樣操作insert id=3的操作,他們都是對id = 3這一行進行操作,也就是說A事務執行insert id =3時發生了行鎖,將該行鎖住,所以B事務對id=3這行進行操作時,是不能成功的,因爲A事務並沒有釋放鎖。

3.如果提交A事務

我們會立馬看到下面的錯誤信息
在這裏插入圖片描述
A事務提交後,id = 3的記錄被持久化到數據庫中。所以B事務再插入id = 3的就報錯了,因爲id是主鍵。

在發生鎖衝突時,可以執行show status like ‘innodb_row_lock%’; 在這裏插入圖片描述
查看鎖的相關信息

十.補充

1.InnoDB怎麼解決幻讀?

Innodb實現了MVCC(多版本併發控制),使用next-key鎖。

  • Select操作不會更新版本號,是快照讀
  • Insert/Update/Delete操作會更新版本號,是當前讀

下面推薦兩篇文章來了解學習。
Innodb中的MVCC
輕鬆理解MYSQL MVCC 實現機制

2.關於快照讀和當前讀

MySQL 在InnoDB引擎下有當前讀和快照讀兩種模式。 當前讀即加鎖讀,讀取記錄的最新版本號,會加鎖保證其他併發事務不能修改當前記錄,直至釋放鎖。插入/更新/刪除操作默認使用當前讀,顯示的爲select語句加lock in share mode或for update的查詢也採用當前讀模式。 快照讀:不加鎖,讀取記錄的快照版本,而非最新版本,使用MVCC機制,最大的好處是讀取不需要加鎖,讀寫不衝突,用於讀操作多於寫操作的應用,因此在不顯示加[lock in share mode]/[for update]的select語句,即普通的一條select語句默認都是使用快照讀MVCC實現模式。
------引自CSDN用戶Aubade2017對博客的評論

很顯然,快照讀和當前讀是兩種讀取機制。快照讀使用了MVCC,讀取不用加鎖。
主要實現是通過在事務操作的當前行的記錄上加上 DATA_TRX_ID 作爲當前事務版本號(可以理解爲創建時間),DATA_ROLL_PTR (可以理解爲刪除時間)作爲回滾指針指向undo log的row_id。(指向當前記錄項的rollback segment的undo log記錄,找之前版本的數據就是通過這個指針)

3.update的事務過程

begin->用排他鎖鎖定該行->記錄redo log->記錄undo log->修改當前行的值,記錄事務編號,回滾指針指向undo log中的修改前的行

注意:

insert的事務過程和update基本一致,只是insert時undo log是不能指向原始記錄的,因爲原始記錄就不存在。所以insert操作的回滾,需要丟棄undo log。

4.select的原則及疑問

Innodb檢查每行數據,確保他們符合兩個標準:
1、InnoDB只查找版本早於當前事務版本的數據行(也就是數據行的版本必須小於等於事務的版本),這確保當前事務讀取的行都是事務之前已經存在的,或者是由當前事務創建或修改的行
2、行的刪除操作的版本一定是未定義的或者大於當前事務的版本號,確定了當前事務開始之前,行沒有被刪除
符合了以上兩點則返回查詢結果。

但我之前一直有個疑問。比如A事務先開啓,執行一個insert操作,假如此時的事務版本號是1,那麼insert的新記錄的DATA_TRX_ID就是1。此時開啓B事務(B事務假如編號爲2),在B事務中查詢,爲什麼沒查詢到小於當前事務版本號(2)的記錄呢?

A事物插入數據是要加入排它鎖的,B通過一致性非鎖定讀的時候不會讀到這個鎖定的數據,會去讀這個數據的快照,即通過undo log來讀取,然而**這條數據是新插入的所以undo log不存在所以讀不到**,這就不會導致髒讀。至於幻讀innoDB是通過next key 鎖來避免的,一致性非鎖定讀同樣讀取的是undo log當中的快照
-----引自CSDN用戶wulei93對博客的評論

上面的這段評論解決了我的疑問。

5.mysql的間隙鎖

推薦下面這篇文章
mysql鎖 innodb下的記錄鎖,間隙鎖,next-key鎖

6.結語

感謝閱讀,有理解有誤的地方希望積極指正,謝謝。在後續深入學習中有心得會再分享

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