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 | 開始事務 Abegin; |
|
T3 | 小王轉款給小明 500 元update users set money=money-500 where id = 1; update users set money=money+500 where id = 2; |
|
T4 | 開始事務 Bbegin; |
|
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 | 開始事務 Abegin; |
|
T3 | 查詢小明餘額select * from users where id = 2; 餘額為 0 元 |
|
T4 | 開始事務 Bbegin; |
|
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 | 開始事務 Abegin; |
|
T3 | 開始事務 Bbegin; |
|
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 鎖相關的內容。