1. MySQL事務隔離級別學習筆記
1.1. 隔離級別
-
READ UNCOMMITTED 未提交讀
在READ UNCOMMITTED級別,事務中的修改,即使沒有提交,對其他事務也都是可見的。事務可以讀取未提交的數據,這也被稱作臟讀。這個級別會導致很多問題,從性能上來說,READ UNCOMMITTED不會比其他的級別好太多,但確缺乏其他級別的很多好處,除非真的有必要的理由,在實際應用中一般很少使用。
-
READ COMMITED 提交讀
大多數數據庫系統的默認隔離級別是 READ COMMITED(但mysql不是)。READ COMMITED滿足前面提到的隔離性簡單定義:一個事務開始時,只能看見已經提交的事務所做的修改。換句話說,一個事務從開始直到提交之前,所做的任何修改對其他事務都是不可見的。這個級別有時候也叫做不可重復讀,因為兩次執行同樣的查詢,可能會得到不一樣的結果。
這里的原因是,在兩次執行同樣的查詢中間,可能由其他事務修改了影響查詢結果的數據。在 Read Commited級別下,會發生這種情況。
-
REPEATABLE READ 可重復讀
MySQL的默認事務隔離級別。
REPEATABLE READ解決了臟讀的問題。該級別保證了在同一個事務中多次讀取同樣的記錄的結果是一致的。但是理論上,可重復讀隔離級別還是無法解決另一個幻讀的問題。所謂幻讀,指的是當某個事務在讀取某個范圍內的記錄時,另外一個事務又在該范圍內插入了新的記錄,當之前的事務再次讀取該范圍的記錄時,會產生幻行。InnoDB存儲引擎通過多版本并發控制(MVCC)解決了幻讀的問題。在一個事務未提交前,多次查詢相同的查詢,都會返回相同的結果。即使在此期間有其他事務已提交了影響該查詢的數據。
-
SERIALIZABLE 可串行化
SERIALIZABLE 是最高的隔離級別。它通過強事務串行執行,避免了前面說的幻讀的問題。簡單來說,SERIALIZABLE會在曲度的每一行數據都加上鎖,所以可能導致大量的超時和鎖爭用的問題。實際應用中也很少用到這個隔離級別,只有在非常需要確保數據一致性而且可以接受沒有并發的情況下,才考慮使用該級別。
1.2. 實驗
上面是摘自《高性能MySQL》一書,光這么看,可能也理解不到多少東西,不如直接做實驗親身體驗一下,加深理解。
我們先簡單創建一個表 users,結構如下:
+------------+------------------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+-------------------+----------------+
| id | int(11) unsigned | NO | PRI | <null> | auto_increment |
| name | varchar(50) | NO | | | |
| created_at | datetime | NO | | CURRENT_TIMESTAMP | |
| updated_at | datetime | NO | | CURRENT_TIMESTAMP | |
| deleted_at | datetime | YES | | <null> | |
+------------+------------------+------+-----+-------------------+----------------+
實現插入三條數據:
+----+-------------+---------------------+---------------------+------------+
| id | name | created_at | updated_at | deleted_at |
+----+-------------+---------------------+---------------------+------------+
| 1 | coolcao2018 | 2018-05-28 12:48:43 | 2018-05-28 12:48:43 | <null> |
| 2 | tom | 2018-05-28 13:24:28 | 2018-05-28 13:24:28 | <null> |
| 3 | lili | 2018-05-31 08:54:28 | 2018-05-31 08:54:28 | <null> |
+----+-------------+---------------------+---------------------+------------+
然后打開兩個終端,A和B分別表示兩個用戶同時在操作。
1.2.1. READ UNCOMMITTED 未提交讀
對于用戶A,操作:
mysql root@localhost:test> set session transaction isolation level read uncommitted;
mysql root@localhost:test> start transaction;
mysql root@localhost:test> select * from users;
+----+-------------+---------------------+---------------------+------------+
| id | name | created_at | updated_at | deleted_at |
+----+-------------+---------------------+---------------------+------------+
| 1 | coolcao2018 | 2018-05-28 12:48:43 | 2018-05-28 12:48:43 | <null> |
| 2 | tom | 2018-05-28 13:24:28 | 2018-05-28 13:24:28 | <null> |
| 3 | lili | 2018-05-31 08:54:28 | 2018-05-31 08:54:28 | <null> |
+----+-------------+---------------------+---------------------+------------+
然后,用戶B操作,
mysql root@localhost:test> set session transaction isolation level read uncommitted;
mysql root@localhost:test> start transaction;
mysql root@localhost:test> update users set name='coolcao' where id=1;
此時,用戶B并未提交事務,用戶A進行查詢操作看看:
mysql root@localhost:test> select * from users;
+----+---------+---------------------+---------------------+------------+
| id | name | created_at | updated_at | deleted_at |
+----+---------+---------------------+---------------------+------------+
| 1 | coolcao | 2018-05-28 12:48:43 | 2018-05-28 12:48:43 | <null> |
| 2 | tom | 2018-05-28 13:24:28 | 2018-05-28 13:24:28 | <null> |
| 3 | lili | 2018-05-31 08:54:28 | 2018-05-31 08:54:28 | <null> |
+----+---------+---------------------+---------------------+------------+
從整個過程來看,用戶B還并未提交事務,但是A卻已經能夠直接讀到B的更新。
從上面實驗結果來看,不難理解上面對于 READ UNCOMMITTED級別的描述:在READ UNCOMMITTED級別,事務中的修改,即使沒有提交,對其他事務也都是可見的。
1.2.2. READ COMMITTED 提交讀
同時將A,B兩個終端的事務級別設置為 read committed:
// 在A,B兩個終端都執行
set session transaction isolation level read committed;
對于A,我們開啟一個事務,然后更新一下數據,但并不提交事務:
mysql root@localhost:test> set session transaction isolation level read committed;
mysql root@localhost:test> start transaction;
mysql root@localhost:test> select * from users;
+----+---------+---------------------+---------------------+------------+
| id | name | created_at | updated_at | deleted_at |
+----+---------+---------------------+---------------------+------------+
| 1 | coolcao | 2018-05-28 12:48:43 | 2018-05-28 12:48:43 | <null> |
| 2 | tom | 2018-05-28 13:24:28 | 2018-05-28 13:24:28 | <null> |
| 3 | lili | 2018-05-31 08:54:28 | 2018-05-31 08:54:28 | <null> |
+----+---------+---------------------+---------------------+------------+
mysql root@localhost:test> update users set name='coolcao222' where id=1;
mysql root@localhost:test> select * from users;
+----+------------+---------------------+---------------------+------------+
| id | name | created_at | updated_at | deleted_at |
+----+------------+---------------------+---------------------+------------+
| 1 | coolcao222 | 2018-05-28 12:48:43 | 2018-05-28 12:48:43 | <null> |
| 2 | tom | 2018-05-28 13:24:28 | 2018-05-28 13:24:28 | <null> |
| 3 | lili | 2018-05-31 08:54:28 | 2018-05-31 08:54:28 | <null> |
+----+------------+---------------------+---------------------+------------+
然后,在B終端,開啟另外一個事務,進行數據查詢:
mysql root@localhost:test> set session transaction isolation level read committed;
mysql root@localhost:test> start transaction;
mysql root@localhost:test> select * from users;
+----+---------+---------------------+---------------------+------------+
| id | name | created_at | updated_at | deleted_at |
+----+---------+---------------------+---------------------+------------+
| 1 | coolcao | 2018-05-28 12:48:43 | 2018-05-28 12:48:43 | <null> |
| 2 | tom | 2018-05-28 13:24:28 | 2018-05-28 13:24:28 | <null> |
| 3 | lili | 2018-05-31 08:54:28 | 2018-05-31 08:54:28 | <null> |
+----+---------+---------------------+---------------------+------------+
然后,將A事務提交:
commit;
這時,再在B查詢 :
mysql root@localhost:test> select * from users;
+----+------------+---------------------+---------------------+------------+
| id | name | created_at | updated_at | deleted_at |
+----+------------+---------------------+---------------------+------------+
| 1 | coolcao222 | 2018-05-28 12:48:43 | 2018-05-28 12:48:43 | <null> |
| 2 | tom | 2018-05-28 13:24:28 | 2018-05-28 13:24:28 | <null> |
| 3 | lili | 2018-05-31 08:54:28 | 2018-05-31 08:54:28 | <null> |
+----+------------+---------------------+---------------------+------------+
從結果來看,也不難理解 read committed級別,對于一個事務,只能讀取到當前事務的數據和其他已經提交的事務的數據,對于其他未提交事務的數據,讀不到。
而且,從上面的實驗結果中,我們也看到了,會話B在會話A提交事務前后查詢的結果并不一致,這也就是上面所說的,不可重復讀。
1.2.3. REPEATABLE READ 可重復讀
我們將會話A設置為REPEATABLE READ :
mysql root@localhost:test> set session transaction isolation level repeatable read;
mysql root@localhost:test> start transaction;
mysql root@localhost:test> select * from users;
+----+---------+---------------------+---------------------+------------+
| id | name | created_at | updated_at | deleted_at |
+----+---------+---------------------+---------------------+------------+
| 1 | coolcao | 2018-08-10 15:21:02 | 2018-08-10 15:21:02 | <null> |
| 2 | tom | 2018-08-10 15:21:07 | 2018-08-10 15:21:07 | <null> |
| 3 | lili | 2018-08-10 15:21:11 | 2018-08-10 15:21:11 | <null> |
+----+---------+---------------------+---------------------+------------+
此時,我們在B終端插入一條數據:
mysql root@localhost:test> insert into users (id,name) values (4,'coco');
mysql root@localhost:test> commit;
mysql root@localhost:test> select * from users;
+----+---------+---------------------+---------------------+------------+
| id | name | created_at | updated_at | deleted_at |
+----+---------+---------------------+---------------------+------------+
| 1 | coolcao | 2018-08-10 15:21:02 | 2018-08-10 15:21:02 | <null> |
| 2 | tom | 2018-08-10 15:21:07 | 2018-08-10 15:21:07 | <null> |
| 3 | lili | 2018-08-10 15:21:11 | 2018-08-10 15:21:11 | <null> |
| 4 | coco | 2018-08-10 15:23:50 | 2018-08-10 15:23:50 | <null> |
+----+---------+---------------------+---------------------+------------+
在終端B中,插入一條記錄,并提交,這時id=4的用戶已經被插入到數據庫。
此時,再回到終端A,查詢:
mysql root@localhost:test> select * from users;
+----+---------+---------------------+---------------------+------------+
| id | name | created_at | updated_at | deleted_at |
+----+---------+---------------------+---------------------+------------+
| 1 | coolcao | 2018-08-10 15:21:02 | 2018-08-10 15:21:02 | <null> |
| 2 | tom | 2018-08-10 15:21:07 | 2018-08-10 15:21:07 | <null> |
| 3 | lili | 2018-08-10 15:21:11 | 2018-08-10 15:21:11 | <null> |
+----+---------+---------------------+---------------------+------------+
哎,查詢的結果中,沒有B剛插入的id=4的用戶,這也就是說該級別的事務隔離,保證了在同一個事務中多次讀取同樣的記錄的結果是一致的。這時,我們在A中插入一條記錄:
mysql root@localhost:test> insert into users (id,name) values (4,'coco');
(1062, u"Duplicate entry '4' for key 'PRIMARY'")
哎,這個時候,數據庫報錯了,提示主鍵重復。明明我在這個事務中,查詢的數據只有1,2,3,為什么插入4的時候提示主鍵沖突呢?是發生幻覺了么?是的,發生“幻讀”了。由于REPEATABLE READ級別的隔離,在一個事務中,多次讀取同樣記錄的結果是一致的,在這多次讀取之間,被別的事務插入了新的數據,這時前事務再插入數據,必然會導致錯誤。
在一個事務未提交前,多次查詢相同的查詢,返回的結果是相同的,即使在此期間,其他事務已經提交了影響該查詢的數據。
1.2.4. SERIALIZABLE 可串行化
我們將A,B同時設置為SERIALIZABLE, 然后在A開啟是個事務,做一個簡單查詢:
mysql root@localhost:test> set session transaction isolation level serializable;
mysql root@localhost:test> start transaction;
mysql root@localhost:test> select * from users where id<10;
+----+---------+---------------------+---------------------+------------+
| id | name | created_at | updated_at | deleted_at |
+----+---------+---------------------+---------------------+------------+
| 1 | coolcao | 2018-08-10 15:21:02 | 2018-08-10 15:21:02 | <null> |
| 2 | tom | 2018-08-10 15:21:07 | 2018-08-10 15:21:07 | <null> |
| 3 | lili | 2018-08-10 15:21:11 | 2018-08-10 15:21:11 | <null> |
| 4 | coco | 2018-08-10 15:23:50 | 2018-08-10 15:23:50 | <null> |
| 5 | juli | 2018-08-10 15:31:32 | 2018-08-10 15:31:32 | <null> |
+----+---------+---------------------+---------------------+------------+
此時,A事務并未提交,然后在B再開啟一個事務,進行插入操作:
mysql root@localhost:test> set session transaction isolation level serializable;
mysql root@localhost:test> start transaction;
mysql root@localhost:test> insert into users (id,name) values (6,'kate');
(1205, u'Lock wait timeout exceeded; try restarting transaction')
你會發現,哎我去,B事務被掛住了,然后過了一段時間,提示了錯誤 (1205, u'Lock wait timeout exceeded; try restarting transaction')
,說等待鎖超時。
是的,在串行化級別,會在讀取的每一行數據都加上鎖,也就是說,上面A事務在讀取時,已經加了鎖,此時B事務在插入操作時,得等待鎖的放開,時間一長,A鎖未放開,B就報錯了。
從實驗中可以看出,可串行化級別,由于要保證避免幻讀而加了鎖導致效率以及可能會觸發的等待鎖超時等錯誤,實際應用中,該級別的事務隔離也很少使用。
對照著實驗結果,來理解上面四個隔離級別,就容易理解了。