髒讀、幻讀和不可重複讀

1、引言

髒讀、不可重複讀和幻讀是數據庫中因爲併發訪問致使的數據讀取問題。當多個事務同時進行時能夠經過修改數據庫事務的隔離級別來處理這三個問題。數據庫

2、問題解釋

一、髒讀(讀取未提交的數據)

髒讀又稱無效數據的讀出,是指在數據庫訪問中,事務 A 對一個值作修改,事務 B 讀取這個值,可是因爲某種緣由事務 A 回滾撤銷了對這個值得修改,這就致使事務 B 讀取到的值是無效數據。併發

二、不可重複讀(先後數據屢次讀取,結果集內容不一致)

不可重複讀即當事務 A 按照查詢條件獲得了一個結果集,這時事務 B 對事務 A 查詢的結果集數據作了修改操做,以後事務 A 爲了數據校驗繼續按照以前的查詢條件獲得的結果集與前一次查詢不一樣,致使不可重複讀取原始數據。性能

三、幻讀(先後數據屢次讀取,結果集數量不一致)

幻讀是指當事務 A 按照查詢條件獲得了一個結果集,這時事務 B 對事務 A 查詢的結果集數據作新增操做,以後事務 A 繼續按照以前的查詢條件獲得的結果集無緣無故多了幾條數據,好像出現了幻覺同樣。spa

3、事務隔離

在併發條件下會出現上述問題,如何着手解決他們保證咱們程序運行的正確性是很是重要的。數據庫提供了 Read uncommitted 、Read committed 、Repeatable read 、Serializable 四種事務隔離級別來解決髒讀、幻讀和不可重複讀問題,同時容易想到,能夠經過加鎖的方式實現事務隔離。.net

在數據庫的增刪改查操做中,insert 、delete 、update 都會加排他鎖,排它鎖會阻止其餘事務對其加鎖的數據加任何類型的鎖。而 select 只有顯示聲明纔會加鎖。code

  • Read uncommitted對象

    讀未提交,說的是一個事務能夠讀取到另外一個事務未提交的數據修改。

    讀若不顯式聲明是不加鎖的,能夠直接讀取到另外一個事務對數據的操做,沒有避免髒讀、不可重複讀、幻讀。blog

  • Read committed索引

    讀已提交,說的是一個事務只能讀取到另外一個事務已經提交的數據修改。

    很明顯,這種隔離級別避免了髒讀,可是可能會出現不可重複讀、幻讀。生命週期

  • Repeatable read

    可重複讀,保證了同一事務下屢次讀取相同的數據返回的結果集是同樣的。

    這種隔離級別解決了髒讀和不可重複讀問題,可是扔有可能出現幻讀。

  • Serializable

    串行化,對同一數據的讀寫全加鎖,即對同一數據的讀寫全是互斥了,數據可靠行很強,可是併發性能不忍直視。

    這種隔離級別雖然解決了上述三個問題,可是犧牲了性能。

總結以下表: √ 表明可能出現,× 表明不會出現。

隔離級別 髒讀 不可重複讀 幻讀
Read uncommitted
Read committed ×
Repeatable read × ×
Serializable × × ×

4、MySQL 事務隔離級別的實現

在 MySQL 中只有 InnoDB 存儲引擎支持事務,可是在平常使用 MySQL 時咱們好像沒有怎麼關心過上述三個問題啊...

緣由很簡單,MySQL 默認 Repeatable read 隔離級別,使用了 MVCC 技術,而且解決了幻讀問題。

MVCC


MVCC 全名多版本併發控制,使用它能夠保證 InnoDB 存儲引擎下讀操做的一致性。使用 MVCC 能夠查詢被另外一個事務修改的行數據,而且能夠查看這些行被更新以前的數據,值得注意的是使用 MVCC 增長了多事務的併發性能,可是並無解決幻讀問題

一、原理

MVCC 是經過保存數據在某個時間點的快照來實現的。也就是說在同一個事務的生命週期中,數據的快照始終是相同的;而在多個事務中,因爲事務的時間點極可能不相同,數據的快照也不盡相同。

二、實現細節

  • 每行數據都存在一個版本,每次數據更新時都更新該版本。
  • 修改時Copy出當前版本隨意修改,各個事務之間互不干擾。
  • 保存時比較版本號,若是成功(commit),則覆蓋原記錄;失敗則放棄copy(rollback)。

經過上面特色咱們能夠看出,MVCC 其實就是相似樂觀鎖的一種實現。

三、InnoDB 中 MVCC 實現

在 InnoDB 中爲每行增長兩個隱藏的字段,分別是該行數據建立時的版本號刪除時的版本號,這裏的版本號是系統版本號(能夠簡單理解爲事務的 ID),每開始一個新的事務,系統版本號就自動遞增,做爲事務的 ID 。一般這兩個版本號分別叫作建立時間和刪除時間。

下面經過具體的例子來幫助理解 InnoDB 中 MVCC 實現,

首先建立一個表:

create table info( 
id int primary key auto_increment, 
name varchar(20));

INSERT
InnoDB 爲新插入的每一行保存當前系統版本號做爲版本號。如今假設事務的版本號從 1 開始。

第一個事務 ID爲1;

start transaction;
insert into info values(NULL,'a');
insert into info values(NULL,'b');
insert into info values(NULL,'c');
commit;

對應在數據中的表以下(後面兩列是隱藏列,也就是版本號)

id name 建立版本(事務ID) 刪除版本(事務ID)
1 a 1 undefined
2 b 1 undefined
3 c 1 undefined
SELECT

InnoDB 會根據下面兩個條件檢查每行記錄:

  • 只會查找版本早於當前事務版本的數據行(行的系統版本號小於或等於事務的系統版本號),這樣能夠確保事務讀取的行,要麼是在事務開始前已經存在的,要麼是事務自身插入或者修改過的
  • 行的刪除版本要麼未定義,要麼大於當前事務版本號,這能夠確保事務讀取到的行,在事務開始以前未被刪除

只有 a, b 同時知足的記錄,才能返回做爲查詢結果.


DELETE

InnoDB會爲刪除的每一行保存當前系統的版本號(事務的ID)做爲刪除標識.
看下面的具體例子分析:

第二個事務 ID爲2;

start transaction;
select * from info;  //(1)
select * from info;  //(2)
commit;
  • 假設1
    假設在執行這個事務 ID 爲 2 的過程當中,剛執行到 (1) ,這時,有另外一個事務 ID 爲 3 往這個表裏插入了一條數據;

第三個事務ID爲3;

start transaction;
insert into info values(NULL,'d');
commit;

這時表中的數據以下:

id name 建立版本(事務ID) 刪除版本(事務ID)
1 a 1 undefined
2 b 1 undefined
3 c 1 undefined
4 d 3 undefined

而後接着執行 事務2 中的 (2) ,因爲 id=4 的數據的建立時間(事務 ID 爲 3 ),執行當前事務的 ID 爲 2 ,而 InnoDB 只會查找事務 ID 小於等於當前事務 ID 的數據行,因此 id=4 的數據行並不會在執行 事務2 中的 (2) 被檢索出來,在 *事務2 *中的兩條 select 語句檢索出來的數據都只會以下表:

id name 建立版本(事務ID) 刪除版本(事務ID)
1 a 1 undefined
2 b 1 undefined
3 c 1 undefined
  • 假設2
    假設在執行這個事務 ID 爲 2 的過程當中,剛執行到 (1) ,假設事務執行完 事務3 後,接着又執行了 事務4 ;

第四個事務:

start   transaction;  
delete from info where id=1;
commit;

此時數據庫中的表數據以下:

id name 建立版本(事務ID) 刪除版本(事務ID)
1 a 1 4
2 b 1 undefined
3 c 1 undefined
4 d 3 undefined

接着執行事務 ID 爲 2 的 事務(2),根據 SELECT 檢索條件能夠知道,它會檢索建立時間(建立事務的 ID )小於當前事務 ID 的行和刪除時間(刪除事務的 ID )大於當前事務的行,而 id=4 的行上面已經說過,而 id=1 的行因爲刪除時間(刪除事務的 ID )大於當前事務的 ID ,因此 事務2 的 (2) select * from info 也會把 id=1 的數據檢索出來。因此,事務2 中的兩條 select 語句檢索出來的數據都以下:

id name 建立版本(事務ID) 刪除版本(事務ID)
1 a 1 4
2 b 1 undefined
3 c 1 undefined

UPDATE

InnoDB 執行 UPDATE,其實是新插入了一行記錄,並保存其建立時間爲當前事務的 ID ,同時保存當前事務 ID 到要 UPDATE 的行的刪除時間。

  • 假設3
    假設在執行完 事務2 的 (1) 後又執行,其它用戶執行了事務 3和 4,這時,又有一個用戶對這張表執行了 UPDATE 操做:

第五個事務:

start  transaction;
update info set name='b' where id=2;
commit;

根據update的更新原則:會生成新的一行,並在原來要修改的列的刪除時間列上添加本事務ID,獲得表以下:

id name 建立版本(事務ID) 刪除版本(事務ID)
1 a 1 4
2 b 1 5
3 c 1 undefined
4 d 3 undefined
2 b 5 undefined

繼續執行 事務2 的 (2) ,根據 select 語句的檢索條件,獲得下表:

id name 建立版本(事務ID) 刪除版本(事務ID)
1 a 1 4
2 b 1 5
3 c 1 undefined

仍是和 事務2 中 (1) select 獲得相同的結果。

❀ 總結:

  • SELECT
    讀取建立版本號小於或等於當前事務版本號,而且刪除版本號爲空或大於當前事務版本號的記錄。如此能夠保證在事務在讀取以前記錄是存在的。
  • INSERT
    將當前事務的版本號保存至插入行的建立版本號。
  • UPDATE
    新插入一行,並以當前事務的版本號做爲新行的建立版本號,同時將原記錄行的刪除版本號設置爲當前事務版本號。
  • DELETE
    將當前事務的版本號保存至行的刪除版本號。

例子參考:https://blog.csdn.net/whoamiy...


四、 InnoDB 如何解決幻讀問題

在 InnoDB 中分爲快照讀當前讀。快照讀讀的是數據的快照,也就是數據的歷史版本;當前讀就是讀的最新版本的數據,而且在讀的時候加鎖,其餘事務都不能對當前行作修改。

  • 快照讀:簡單的 select 操做,屬於快照讀,不加鎖。
    select * from table where ?;
  • 當前讀:特殊的讀操做,插入、更新、刪除操做,屬於當前讀,須要加鎖。
    select * from table where ? lock in share mode;
    select * from table where ? for update;
    insert into table values (…);
    update table set ? where ?;
    delete from table where ?;

對於上面當前讀的語句,第一條讀取記錄加共享鎖,其餘的所有加排它鎖。

也就是說在作數據的修改操做時,都會使用當前讀的方式,當前讀是經過行鎖和間隙鎖控制的,此時是加了排他鎖的,全部其餘的事務都不能動當前的事務,因此避免了出現幻讀的可能。

而爲了防止幻讀,行鎖和間隙鎖扮演了重要角色,下面簡單說一下:

  • 行鎖
    字面意思簡單理解對數據行加鎖,注意 InnoDB 行鎖是經過給索引上的索引項加鎖來實現的,也就是說只有經過索引條件檢索數據,InnoDB才使用行級鎖,不然,InnoDB將使用表鎖!
  • 間隙鎖
    間隙鎖就是用來爲數據行之間的間隙來進行加鎖。

舉個例子:

select * from info where id > 5;

上面 SQL 中,其中 id 是主鍵,假設在一個 事務 A 中執行這個查詢,第一次查詢爲一個 結果集 1 。在作第二次查詢時,另外一個 事務 B 在 info 表進行了插入數據 7 和 10 的操做。在 事務 A 再次執行此查詢查詢出 結果集 2 的時候,發現多了幾條記錄,如此便產生了幻讀。

  • 結果集1
6,8,9
  • 結果集2
6,7,8,9,10

因此試想爲了防止幻讀,咱們不但要現存的 id > 5 的數據行(6,8,9)上面加鎖(行鎖),還要在它們的間隙加鎖(間隙鎖)。

咱們以區間來表示要加鎖對象:

(5,6]
(6,8]
(8,9]
(9,+∞)

其中區間的右閉即爲要加的行鎖,而區間的範圍便是要加的間隙鎖。

5、結語

關於髒讀、不可重複讀和幻讀的理解便記錄到這裏了,因筆者水平有限,若有錯誤歡迎指正。

歡迎訪問 我的博客 獲取更多知識分享。
相關文章
相關標籤/搜索