MySQL的事務隔離級別

一、數據庫的隔離級別概述

隔離級別

髒讀(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級別中,是解決了幻讀的讀問題的。參見下圖

innodb_lock_1

讀問題解決了,根據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;

id class_name teacher_id
2 初三二班 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;

id class_name teacher_id
2 初三四班 30
10 初三二班 30


 

RR級別:

事務A 事務B
begin;

begin;

select id,class_name,teacher_id from class_teacher where teacher_id=30;

id class_name teacher_id
2 初三二班 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;

id class_name teacher_id
2 初三四班 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_lock_2

如圖所示,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;

id class_name teacher_id
1 初三一班

5

2 初三二班 30
   
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級別在寫數據時的幻讀問題。


參考:http://blog.csdn.net/matt8/article/details/53096405

InnoDB加鎖分析

發佈了177 篇原創文章 · 獲贊 405 · 訪問量 94萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章