髒讀、不可重複讀和幻讀是數據庫中因爲併發訪問致使的數據讀取問題。當多個事務同時進行時能夠經過修改數據庫事務的隔離級別來處理這三個問題。數據庫
髒讀又稱無效數據的讀出,是指在數據庫訪問中,事務 A 對一個值作修改,事務 B 讀取這個值,可是因爲某種緣由事務 A 回滾撤銷了對這個值得修改,這就致使事務 B 讀取到的值是無效數據。併發
不可重複讀即當事務 A 按照查詢條件獲得了一個結果集,這時事務 B 對事務 A 查詢的結果集數據作了修改操做,以後事務 A 爲了數據校驗繼續按照以前的查詢條件獲得的結果集與前一次查詢不一樣,致使不可重複讀取原始數據。性能
幻讀是指當事務 A 按照查詢條件獲得了一個結果集,這時事務 B 對事務 A 查詢的結果集數據作新增操做,以後事務 A 繼續按照以前的查詢條件獲得的結果集無緣無故多了幾條數據,好像出現了幻覺同樣。spa
在併發條件下會出現上述問題,如何着手解決他們保證咱們程序運行的正確性是很是重要的。數據庫提供了 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 | × | × | × |
在 MySQL 中只有 InnoDB 存儲引擎支持事務,可是在平常使用 MySQL 時咱們好像沒有怎麼關心過上述三個問題啊...
緣由很簡單,MySQL 默認 Repeatable read 隔離級別,使用了 MVCC 技術,而且解決了幻讀問題。
MVCC 全名多版本併發控制,使用它能夠保證 InnoDB 存儲引擎下讀操做的一致性。使用 MVCC 能夠查詢被另外一個事務修改的行數據,而且能夠查看這些行被更新以前的數據,值得注意的是使用 MVCC 增長了多事務的併發性能,可是並無解決幻讀問題。
MVCC 是經過保存數據在某個時間點的快照來實現的。也就是說在同一個事務的生命週期中,數據的快照始終是相同的;而在多個事務中,因爲事務的時間點極可能不相同,數據的快照也不盡相同。
經過上面特色咱們能夠看出,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;
第三個事務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 |
第四個事務:
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 的行的刪除時間。
第五個事務:
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 中分爲快照讀和當前讀。快照讀讀的是數據的快照,也就是數據的歷史版本;當前讀就是讀的最新版本的數據,而且在讀的時候加鎖,其餘事務都不能對當前行作修改。
- 快照讀:簡單的 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 ?;
對於上面當前讀的語句,第一條讀取記錄加共享鎖,其餘的所有加排它鎖。
也就是說在作數據的修改操做時,都會使用當前讀的方式,當前讀是經過行鎖和間隙鎖控制的,此時是加了排他鎖的,全部其餘的事務都不能動當前的事務,因此避免了出現幻讀的可能。
而爲了防止幻讀,行鎖和間隙鎖扮演了重要角色,下面簡單說一下:
舉個例子:
select * from info where id > 5;
上面 SQL 中,其中 id 是主鍵,假設在一個 事務 A 中執行這個查詢,第一次查詢爲一個 結果集 1 。在作第二次查詢時,另外一個 事務 B 在 info 表進行了插入數據 7 和 10 的操做。在 事務 A 再次執行此查詢查詢出 結果集 2 的時候,發現多了幾條記錄,如此便產生了幻讀。
6,8,9
6,7,8,9,10
因此試想爲了防止幻讀,咱們不但要現存的 id > 5 的數據行(6,8,9)上面加鎖(行鎖),還要在它們的間隙加鎖(間隙鎖)。
咱們以區間來表示要加鎖對象:
(5,6]
(6,8]
(8,9]
(9,+∞)
其中區間的右閉即爲要加的行鎖,而區間的範圍便是要加的間隙鎖。
關於髒讀、不可重複讀和幻讀的理解便記錄到這裏了,因筆者水平有限,若有錯誤歡迎指正。
歡迎訪問 我的博客 獲取更多知識分享。