一、數據庫的隔離級別概述
隔離級別 |
髒讀(Dirty Read) |
不可重複讀(NonRepeatable Read) |
幻讀(Phantom Read) |
未提交讀(Read uncommitted)--(髒讀) |
可能 |
可能 |
可能 |
已提交讀(Read committed)--(不可重複讀) |
不可能 |
可能 |
可能 |
可重複讀(Repeatable read) |
不可能 |
不可能 |
可能 |
可串行化(Serializable) |
不可能 |
不可能 |
不可能 |
二、未提交讀(Read uncommited) -- (髒讀)
會出現髒讀,也就是可能讀取到其他會話中未提交事務修改的數據,數據庫一般都不會用,而且任何操作都不會加鎖
分別在A、B兩個客戶端執行:
A:
root@(none) 10:54>SET GLOBAL tx_isolation='READ-UNCOMMITTED';
Query OK, 0 rows affected (0.00 sec)
root@(none) 10:54>SELECT @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set (0.00 sec)
//開啓事務
root@test 10:55>begin;
Query OK, 0 rows affected (0.00 sec)
root@test 10:55>select * from test1;
+------+
| a |
+------+
| 1 |
| 2 |
| 3 |
| 4 |
+------+
4 rows in set (0.00 sec)
B上:
root@test 10:58>select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set (0.00 sec)
root@test 10:58>
root@test 10:58>begin;
Query OK, 0 rows affected (0.00 sec)
root@test 10:58>insert into test.test1 values (999);
Query OK, 1 row affected (0.00 sec)
root@test 10:58>select * from test.test1;
+------+
| a |
+------+
| 1 |
| 2 |
| 3 |
| 4 |
| 999 |
+------+
5 rows in set (0.00 sec)
此處B客戶端並未commit;
再查看A客戶端:
root@test 10:58>select * from test1;
+------+
| a |
+------+
| 1 |
| 2 |
| 3 |
| 4 |
| 999 |
+------+
5 rows in set (0.00 sec)
此處A可以看到新的記錄了。
可見,客戶端B中新增了記錄,未commit,此時客戶端A卻讀出了新增的數據,如果此時客戶端B取消掉當前的記錄,那麼數據庫中就不存在該記錄,然而客戶端A卻擁有了該數據,這就是髒讀!
三、已提交讀(Read commited) -- (不可重複讀)
這是大多數數據庫的默認隔離級別(除了MySQL),簡單的理解就是,只能讀取到最新的數據
在A客戶端:
root@(none) 11:10>SET GLOBAL tx_isolation='READ-COMMITTED';
Query OK, 0 rows affected (0.00 sec)
root@(none) 11:10>SELECT @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| READ-COMMITTED |
+----------------+
1 row in set (0.00 sec)
root@(none) 11:10>
root@(none) 11:10>begin;
Query OK, 0 rows affected (0.00 sec)
root@(none) 11:10>select * from test.test1;
+------+
| a |
+------+
| 1 |
| 2 |
| 3 |
| 4 |
+------+
在B客戶端執行:
root@test 11:11>begin;
Query OK, 0 rows affected (0.00 sec)
root@test 11:11>select * from test.test1;
+------+
| a |
+------+
| 1 |
| 2 |
| 3 |
| 4 |
+------+
4 rows in set (0.00 sec)
root@test 11:11>
root@test 11:11>delete from test.test1 where a=1;
Query OK, 1 row affected (0.00 sec)
root@test 11:12>select * from test.test1;
+------+
| a |
+------+
| 2 |
| 3 |
| 4 |
+------+
此時查詢A客戶端:
root@(none) 11:12>select * from test.test1;
+------+
| a |
+------+
| 1 |
| 2 |
| 3 |
| 4 |
+------+
此處看出A客戶端無變化,在B客戶端執行commit後再查看A客戶端:
root@(none) 11:13>select * from test.test1;
+------+
| a |
+------+
| 2 |
| 3 |
| 4 |
+------+
可以看到A客戶端的數據已經變了。已提交讀只允許讀取已提交的記錄,但不要求可重複讀。
用MVCC來說就是讀取當前行的最新版本。
可以看到,已提交讀,是讀到只能讀取到最新的數據;
當客戶端B刪除了記錄時未commit時,此時數據庫中最新的數據還是原來的數據,所以客戶端A讀取到的還是原來的數據;
當客戶單B commit之後,此時數據庫中最新的數據已經是刪除後的數據了,所以客戶端A讀取到的是刪除後的數據;
四、可重複讀(Repeatable read )
這是MySQL Innodb的默認隔離級別,在同一個事務內的查詢都是事務開始時刻一致的。在SQL標準中,該隔離級別消除了不可重複讀,但是還存在幻象讀,然而,在MySQL中通過InnoDB和Falcon存儲引擎通過多版本併發控制(MVCC,Multiversion Concurrency Control)機制解決了該問題。
在A客戶端上:
root@(none) 11:17>SET GLOBAL tx_isolation='REPEATABLE-READ';
Query OK, 0 rows affected (0.00 sec)
root@(none) 11:17>
root@(none) 11:17>
root@(none) 11:17>SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)
root@(none) 11:17>BEGIN;
Query OK, 0 rows affected (0.00 sec)
在B客戶端上:
root@test 11:20>select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)
root@test 11:20>insert into test.test1 values (555);
Query OK, 1 row affected (0.00 sec)
root@test 11:20>commit;
Query OK, 0 rows affected (0.00 sec)
root@test 11:21>
root@test 11:21>select * from test.test1;
+------+
| a |
+------+
| 2 |
| 3 |
| 4 |
| 555 |
+------+
4 rows in set (0.00 sec)
此處在B客戶端上已經commit.
然後查看A客戶端:
root@(none) 11:22>SELECT * FROM test.test1;
+------+
| a |
+------+
| 2 |
| 3 |
| 4 |
+------+
3 rows in set (0.00 sec)
root@(none) 11:22>commit;
Query OK, 0 rows affected (0.00 sec)
root@(none) 11:22>SELECT * FROM test.test1;
+------+
| a |
+------+
| 2 |
| 3 |
| 4 |
| 555 |
+------+
4 rows in set (0.00 sec)
在A客戶端上提交後可以看到新數據。
也就是說在可重複讀隔離級別只能讀取已經提交的數據,並且在一個事務內,讀取的數據就是事務開始時的數據。
可以看到,事務A一開始讀的數據,接着事務B對數據進行修改,但是並沒有影響到事務A的讀的數據,也就是說,在Repeatable read的隔離級別上,在一個事務內,
五、Serializable(可串行化) 不使用
是最高的隔離級別,它通過強制事務排序,使之不可能相互衝突,從而解決幻讀問題。簡言之,它是在每個讀的數據行上加上共享鎖。在這個級別,可能導致大量的超時現象和鎖競爭。
該類型在A客戶端操作test.test1表時會鎖定該數據,如果B客戶端想要操作test.test1就需要等待A客戶端釋放。
六、什麼是幻讀?
幻讀只要爲insert造成的,當事務不是獨立執行時,發生的現象。
例如,事務A修改員工表的所有員工工資爲1000,修改行數爲10,然後事務B增加一個1000的員工,事務A讀取員工表時,發現,工資1000的員工數時11,造成了幻讀
七、已提交讀和可重複讀的原理
通過鎖機制來實現?
如果使用鎖機制來實現這兩種隔離級別,在可重複讀中,該sql第一次讀取到數據後,就將這些數據加鎖,其它事務無法修改這些數據,就可以實現可重複讀了。但這種方法卻無法鎖住insert的數據,所以當事務A先前讀取了數據,或者修改了全部數據,事務B還是可以insert數據提交,這時事務A就會發現莫名其妙多了一條之前沒有的數據,這就是幻讀,不能通過行鎖來避免。需要Serializable隔離級別
,讀用讀鎖,寫用寫鎖,讀鎖和寫鎖互斥(當數據處於被讀狀態,不能被寫;或者處於被寫狀態,不能被讀),這麼做可以有效的避免幻讀、不可重複讀、髒讀等問題,但會極大的降低數據庫的併發能力。
MySQL、Oracle、PostgreSQL等成熟的數據庫,出於性能考慮,都是使用了以樂觀鎖爲理論基礎的MVCC(多版本併發控制)來避免這兩種問題。
八、多版本併發控制 MVCC
mvcc的實現,是通過保存數據在某個時間點的快照來實現的。(快照讀)
也就是說
1. 無論事務執行多長時間,每一個事務看到的數據都是一樣的
2. 根據事務開始的時間不同,不同的事務對同一張表,同一時刻看到的數據可能不一樣
九、MVCC在MySQL InnoDB的使用(通過樂觀鎖)
在InnoDB中,會在每行數據後添加兩個額外的隱藏的值來實現MVCC,這兩個值一個記錄這行數據何時被創建,另外一個記錄這行數據何時過期(或者被刪除)。 在實際操作中,存儲的並不是時間,而是事務的版本號,每開啓一個新事務,事務的版本號就會遞增。 在可重讀Repeatable reads事務隔離級別下:
- SELECT時,讀取創建版本號<=當前事務版本號,刪除版本號爲空或>當前事務版本號。
- INSERT時,保存當前事務版本號爲行的創建版本號
- DELETE時,保存當前事務版本號爲行的刪除版本號
- UPDATE時,插入一條新紀錄,保存當前事務版本號爲行創建版本號,同時保存當前事務版本號到原來刪除的行
通過MVCC,雖然每行記錄都需要額外的存儲空間,更多的行檢查工作以及一些額外的維護工作,但可以減少鎖的使用,大多數讀操作都不用加鎖,讀數據操作很簡單,性能很好,並且也能保證只會讀取到符合標準的行,也只鎖住必要行。
我們不管從數據庫方面的教課書中學到,還是從網絡上看到,大都是上文中事務的四種隔離級別這一模塊列出的意思,RR級別是可重複讀的,但無法解決幻讀,而只有在Serializable級別才能解決幻讀。於是我就加了一個事務C來展示效果。在事務C中添加了一條teacher_id=1的數據commit,RR級別中應該會有幻讀現象,事務A在查詢teacher_id=1的數據時會讀到事務C新加的數據。但是測試後發現,在MySQL中是不存在這種情況的,在事務C提交後,事務A還是不會讀到這條數據。可見在MySQL的RR級別中,是解決了幻讀的讀問題的。參見下圖
讀問題解決了,根據MVCC的定義,併發提交數據時會出現衝突,那麼衝突時如何解決呢?我們再來看看InnoDB中RR級別對於寫數據的處理。
####“讀”與“讀”的區別
可能有讀者會疑惑,事務的隔離級別其實都是對於讀數據的定義,但到了這裏,就被拆成了讀和寫兩個模塊來講解。這主要是因爲MySQL中的讀,和事務隔離級別中的讀,是不一樣的。
我們且看,在RR級別中,通過MVCC機制,雖然讓數據變得可重複讀,但我們讀到的數據可能是歷史數據,是不及時的數據,不是數據庫當前的數據!這在一些對於數據的時效特別敏感的業務中,就很可能出問題。
對於這種讀取歷史數據的方式,我們叫它快照讀 (snapshot read),而讀取數據庫當前版本數據的方式,叫當前讀 (current read)。很顯然,在MVCC中:
- 快照讀:就是select
- select * from table ....;
- 當前讀:特殊的讀操作,插入/更新/刪除操作,屬於當前讀,處理的都是當前的數據,需要加鎖。
- select * from table where ? lock in share mode;
- select * from table where ? for update;
- insert;
- update ;
- delete;
事務的隔離級別實際上都是定義了當前讀的級別,MySQL爲了減少鎖處理(包括等待其它鎖)的時間,提升併發能力,引入了快照讀的概念,使得select不用加鎖。而update、insert這些“當前讀”,就需要另外的模塊來解決了。
因爲更新數據、插入數據是針對當前數據的,所以不能以快照的歷史數據爲參考,此處就是這個意思。
###寫("當前讀")
事務的隔離級別中雖然只定義了讀數據的要求,實際上這也可以說是寫數據的要求。上文的“讀”,實際是講的快照讀;而這裏說的“寫”就是當前讀了。
爲了解決當前讀中的幻讀問題,MySQL事務使用了Next-Key鎖。
####Next-Key鎖
Next-Key鎖是行鎖和GAP(間隙鎖)的合併,行鎖上文已經介紹了,接下來說下GAP間隙鎖。
行鎖可以防止不同事務版本的數據修改提交時造成數據衝突的情況。但如何避免別的事務插入數據就成了問題。我們可以看看RR級別和RC級別的對比
RC級別:
事務A | 事務B | |||||||||
---|---|---|---|---|---|---|---|---|---|---|
begin; |
begin; |
|||||||||
select id,class_name,teacher_id from class_teacher where teacher_id=30;
|
||||||||||
update class_teacher set class_name='初三四班' where teacher_id=30; | ||||||||||
insert into class_teacher values (null,'初三二班',30); commit; |
||||||||||
select id,class_name,teacher_id from class_teacher where teacher_id=30;
|
RR級別:
事務A | 事務B | ||||||
---|---|---|---|---|---|---|---|
begin; |
begin; |
||||||
select id,class_name,teacher_id from class_teacher where teacher_id=30;
|
|||||||
update class_teacher set class_name='初三四班' where teacher_id=30; | |||||||
insert into class_teacher values (null,'初三二班',30); waiting.... |
|||||||
select id,class_name,teacher_id from class_teacher where teacher_id=30;
|
|||||||
commit; | 事務Acommit後,事務B的insert執行。 |
通過對比我們可以發現,在RC級別中,事務A修改了所有teacher_id=30的數據,但是當事務Binsert進新數據後,事務A發現莫名其妙多了一行teacher_id=30的數據,而且沒有被之前的update語句所修改,這就是“當前讀”的幻讀。
RR級別中,事務A在update後加鎖,事務B無法插入新數據,這樣事務A在update前後讀的數據保持一致,避免了幻讀。這個鎖,就是Gap鎖。
MySQL是這麼實現的:
在class_teacher這張表中,teacher_id是個索引,那麼它就會維護一套B+樹的數據關係,爲了簡化,我們用鏈表結構來表達(實際上是個樹形結構,但原理相同)
如圖所示,InnoDB使用的是聚集索引,teacher_id身爲二級索引,就要維護一個索引字段和主鍵id的樹狀結構(這裏用鏈表形式表現),並保持順序排列。
Innodb將這段數據分成幾個個區間
- (negative infinity, 5],
- (5,30],
- (30,positive infinity);
update class_teacher set class_name='初三四班' where teacher_id=30;不僅用行鎖,鎖住了相應的數據行;同時也在兩邊的區間,(5,30]和(30,positive infinity),都加入了gap鎖。這樣事務B就無法在這個兩個區間insert進新數據。
受限於這種實現方式,Innodb很多時候會鎖住不需要鎖的區間。如下所示:
事務A | 事務B | 事務C | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
begin; | begin; | begin; | |||||||||
select id,class_name,teacher_id from class_teacher;
|
|||||||||||
update class_teacher set class_name='初一一班' where teacher_id=20; | |||||||||||
insert into class_teacher values (null,'初三五班',10); waiting ..... |
insert into class_teacher values (null,'初三五班',40); | ||||||||||
commit; | 事務A commit之後,這條語句才插入成功 | commit; | |||||||||
commit; |
update的teacher_id=20是在(5,30]區間,即使沒有修改任何數據,Innodb也會在這個區間加gap鎖,而其它區間不會影響,事務C正常插入。
如果使用的是沒有索引的字段,比如update class_teacher set teacher_id=7 where class_name='初三八班(即使沒有匹配到任何數據)',那麼會給全表加入gap鎖。同時,它不能像上文中行鎖一樣經過MySQL Server過濾自動解除不滿足條件的鎖,因爲沒有索引,則這些字段也就沒有排序,也就沒有區間。除非該事務提交,否則其它事務無法插入任何數據。
行鎖防止別的事務修改或刪除,GAP鎖防止別的事務新增,行鎖和GAP鎖結合形成的的Next-Key鎖共同解決了RR級別在寫數據時的幻讀問題。