MySQL 事務隔離實驗-認識:髒讀、不可重複讀、幻讀

一隻賤熊貓發表於2018-09-18

0x00 前言

大家也許聽說過 MySQL 的事務在高併發執行的時候可能會發生髒讀不可重複讀幻讀等問題。對於有處理高併發經驗的老鳥,可能認知會更深一些所以覺得 so easy~「老鳥請點紅叉離開,或者發起友好評論O(∩_∩)O哈哈~」,不過對於像我這種難以接觸到高併發業務場景的初學者來說,也就只能看幾篇博文,瞭解一下概念,紙上談兵/(ㄒoㄒ)/~~。不過本著「打破砂鍋問到底」的精神,決定通過做實驗來提高對其理解,順便增強記憶(起碼找工作被問到還能說兩句)。

0x01 MySql 事務隔離級別

MySql 事務隔離級別和允許併發副作用,分別如下表:

事務隔離級別 髒讀 不可重複讀 幻讀
讀未提交(read uncommitted)
不可重複讀(read committed)
可重複讀(repeatable read)
序列化(serializable)

由上表可知,MySQL 共支援四種事務隔離級別。表由上到下允許併發副作用越來越弱,似乎我們只要選擇序列化(serializable)的事務隔離級別就不會發生髒讀不可重複讀幻讀等問題了,但是選擇序列化(serializable)卻會帶來一定的效能下降。所以關於如何選擇事務隔離級別我們需要對髒讀不可重複讀幻讀有一定認知,並確定這幾種副作用對應用的影響,然後選擇合適的隔離級別。

MySQL 的預設事務隔離級別為 可重複讀(repeatable read) 所以我們不用擔心「髒讀」和「不可重複讀」。

查詢 MySQL 事務隔離級別的語句如下:

select @@tx_isolation;
/*
輸出結果:
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
*/
複製程式碼

設定事務隔離級別:

-- 設定事務隔離級別為 read committed,僅在本次會話中生效
set session transaction isolation level read committed;
複製程式碼

或者可以修改 my.cnf 配置檔案使其永久生效。

[mysqld]
transaction-isolation = REPEATABLE-READ
複製程式碼

0x02 實驗環境

本次實驗採用 MySql 5.7.21 版本(儲存引擎為 Innodb),測試資料表結構如下:

/*
+-------+----------+------+-----+---------+----------------+
| Field | Type     | Null | Key | Default | Extra          |
+-------+----------+------+-----+---------+----------------+
| id    | int(11)  | NO   | PRI | <null>  | auto_increment |
| name  | char(20) | NO   |     | <null>  |                |
| money | float    | NO   |     | 0       |                |
+-------+----------+------+-----+---------+----------------+
*/
複製程式碼

0x03 髒讀

髒讀的概念如下:

事務中的修改,即使沒有提交,對其他事務也都是可見的。事務可以讀取未提交的資料,這也被稱作髒讀。

個人認為髒讀的副作用是最大的,現在通過實驗證明髒讀的危害。

實驗users表如下:

/*
+----+------+--------+
| id | name | money  |
+----+------+--------+
| 1  | 小王 | 1000.0 |
| 2  | 小明 |    0.0 |
+----+------+--------+
*/
複製程式碼

實驗步驟表:

時間 客戶端 A 客戶端 B
T1 設定事務隔離級別為 read uncommitted 設定事務隔離級別為 read uncommitted
T2 開始事務 A
begin;
T3 小王轉款給小明 500 元
update users set money=money-500 where id = 1;
update users set money=money+500 where id = 2;
T4 開始事務 B
begin;
T5 查詢小明賬戶餘額
select * from users where id = 2;
查詢結果為 500 元,餘額充足則執行支付邏輯
T6 小明賬戶扣款 100 元
update users set money=money-100 where id = 2;
本條語句將會阻塞
T7 事務 A 回滾
rollback;
語句執行完畢
T8 事務 B 提交
commit;

最後我們查詢users表,結果如下:

/*
+----+------+--------+
| id | name | money  |
+----+------+--------+
| 1  | 小王 | 1000.0 |
| 2  | 小明 | -100.0 |
+----+------+--------+
*/
複製程式碼

令人驚訝的結果,小明的餘額變成了 -100 元!這就是髒讀的危害,我們重點看上表的 T5,發現在事務 A 還未提交之時事務 B 便已經讀取到了事務 A 更新後的結果,這直接導致了我們程式判斷餘額充足從而執行了扣款的邏輯。如果事務 A 成功提交那麼程式結果就是正確的,但是事務 A 最後沒有成功提交而是進行了回滾,這就導致了使用者餘額被扣款為負數的災難。

0x04 不可重複讀

不可重複讀的概念如下:

一個事務開始時,只能看見已經提交的事務所做的修改。換句話說,一個事務從開始直到提交之前,所做的任何修改對其他事務都是不可見的。但是兩次執行同樣的查詢,可能會得到不一樣的結果。

實驗users表如下:

/*
+----+------+--------+
| id | name | money  |
+----+------+--------+
| 1  | 小王 | 1000.0 |
| 2  | 小明 |    0.0 |
+----+------+--------+
*/
複製程式碼

實驗步驟表:

時間 客戶端 A 客戶端 B
T1 設定事務隔離級別為 read committed 設定事務隔離級別為 read committed
T2 開始事務 A
begin;
T3 查詢小明餘額
select * from users where id = 2;
餘額為 0 元
T4 開始事務 B
begin;
T5 小明賬戶充值100元
update users set money=money+100 where id = 2;
T6 事務 B 提交
commit;
T7 查詢小明餘額
select * from users where id = 2;
餘額為 100 元
T8 事務 A 提交
commit;

不可重複讀表現在於在同一個事務之中,兩個相同的查詢得到的查詢結果卻不同。這是由於兩個查詢結果之間,出現另外一個事務修改了包含之前查詢結果的記錄,導致第二次查詢與第一次查詢結果不同。它與髒讀的區別在於修改記錄的事務 B 必須提交成功,查詢事務 A 才能讀取到修改後的記錄,如果事務 B 回滾了,事務 A 的查詢結果還是一樣的。

0x05 幻讀

幻讀概念如下:

所謂幻讀,指的是當某個事務在讀取某個範圍內的記錄時,另外一個事務又在該範圍內插入了新的記錄,當之前的事務再次讀取該範圍的記錄時,會產生幻行。InnoDB儲存引擎通過多版本併發控制(MVCC)解決了幻讀的問題。

經過本人測試發現在 可重複讀(repeatable read)的事務隔離級別下,MySQL 不會產生幻行但是可以通過寫入一行資料來證明幻讀問題的存在。

實驗users表如下:

/*
+----+------+--------+
| id | name | money  |
+----+------+--------+
| 1  | 小王 | 1000.0 |
| 2  | 小明 |    0.0 |
+----+------+--------+
*/
複製程式碼

實驗步驟表:

時間 客戶端 A 客戶端 B
T1 設定事務隔離級別為 repeatable read 設定事務隔離級別為 repeatable read
T2 開始事務 A
begin;
T3 開始事務 B
begin;
T4 插入一行
insert into users(id, name, money) values (3, "小紅",1000);
T5 事務 B 提交
commit;
T6 查詢users
select * from users;
並無 id 為 3 的記錄
T7 插入一行
insert into users(id, name, money) values (3, "小紅",1000);
T8 出現報錯:(1062, u"Duplicate entry '3' for key 'PRIMARY'")

對於事務 A 來說出現的報錯就像見鬼了一樣,因為事務 A 在查詢 users 表的結果並不存在 id 為 3 的行!而在插入該行時卻出現了該行已存在的報錯……也許這就是叫幻讀的原因吧。

0x06 總結

網上已有很多這種型別的文章,本文也參考了許多內容,之所以還要「老調重彈」是因為「紙上得來終覺淺,絕知此事要躬行」,實踐才是檢驗真理的唯一標準,當然本文也可能出現謬誤,歡迎指正。內心 OS:資料庫真的後端的一塊大頭,不想整天 CRUD 就要更深入的學啊。感覺《高效能 MySQL》這本書不錯,有空要研讀一下,最後覺得很有必要學習關於 MySQL 鎖相關的內容。

相關文章