髒讀、不可重複讀和幻讀是數據庫中由于 并發訪問 導緻的數據讀取問題。當多個事務同時進行時可以通過修改數據庫事務的隔離級别來處理這三個問題。
二、問題解釋1、髒讀(讀取未提交的數據)髒讀又稱無效數據的讀出,是指在數據庫訪問中,事務 A 對一個值做修改,事務 B 讀取這個值,但是由于某種原因事務 A 回滾撤銷了對這個值得修改,這就導緻事務 B 讀取到的值是無效數據。
2、不可重複讀(前後數據多次讀取,結果集内容不一緻)不可重複讀即當事務 A 按照查詢條件得到了一個結果集,這時事務 B 對事務 A 查詢的結果集數據做了修改操作,之後事務 A 為了數據校驗繼續按照之前的查詢條件得到的結果集 與前一次查詢不同 ,導緻不可重複讀取原始數據。
3、幻讀(前後數據多次讀取,結果集數量不一緻)幻讀是指當事務 A 按照查詢條件得到了一個結果集,這時事務 B 對事務 A 查詢的結果集數據做新增操作,之後事務 A 繼續按照之前的查詢條件得到的結果集平白無故 多了幾條數據 ,好像出現了幻覺一樣。
三、事務隔離在并發條件下會出現上述問題,如何着手解決他們保證我們程序運行的正确性是非常重要的。數據庫提供了 Read uncommitted 、Read committed 、Repeatable read 、Serializable 四種事務隔離級别來解決髒讀、幻讀和不可重複讀問題,同時容易想到,可以通過加鎖的方式實現事務隔離。
在數據庫的增删改查操作中,insert 、delete 、update 都會加排他鎖, 排它鎖會阻止其他事務對其加鎖的數據加任何類型的鎖 。而 select 隻有顯示聲明才會加鎖。
總結如下表: √ 代表可能出現,× 代表不會出現。
隔離級别髒讀不可重複讀幻讀Read uncommitted√√√Read committed×√√Repeatable read××√Serializable×××
四、MySQL 事務隔離級别的實現在 MySQL 中隻有 InnoDB 存儲引擎支持事務,但是在日常使用 MySQL 時我們好像沒有怎麼關心過上述三個問題啊...
原因很簡單,MySQL 默認 Repeatable read 隔離級别,使用了 MVCC 技術,并且解決了幻讀問題。
MVCCMVCC 全名多版本并發控制,使用它可以保證 InnoDB 存儲引擎下讀操作的一緻性。使用 MVCC 可以查詢被另一個事務修改的行數據,并且可以查看這些行被更新之前的數據,值得注意的是 使用 MVCC 增加了多事務的并發性能,但是并沒有解決幻讀問題 。
1、原理MVCC 是通過保存數據在某個時間點的快照來實現的。也就是說在同一個事務的生命周期中,數據的快照始終是相同的;而在多個事務中,由于事務的時間點很可能不相同,數據的快照也不盡相同。
2、實現細節通過上面特點我們可以看出,MVCC 其實就是類似樂觀鎖的一種實現。
3、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;
對應在數據中的表如下(後面兩列是隐藏列,也就是版本号)
idname創建版本(事務ID)删除版本(事務ID)1a1undefined2b1undefined3c1undefined
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;
這時表中的數據如下:
idname創建版本(事務ID)删除版本(事務ID)1a1undefined2b1undefined3c1undefined4d3undefined
然後接着執行 事務2 中的 (2) ,由于 id=4 的數據的創建時間(事務 ID 為 3 ),執行當前事務的 ID 為 2 ,而 InnoDB 隻會查找事務 ID 小于等于當前事務 ID 的數據行,所以 id=4 的數據行并不會在執行 事務2 中的 (2) 被檢索出來,在 *事務2 *中的兩條 select 語句檢索出來的數據都隻會如下表:
idname創建版本(事務ID)删除版本(事務ID)1a1undefined2b1undefined3c1undefined
start transaction;
delete from info where id=1;
commit;
此時數據庫中的表數據如下:
idname創建版本(事務ID)删除版本(事務ID)1a142b1undefined3c1undefined4d3undefined
接着執行事務 ID 為 2 的 事務(2) ,根據 SELECT 檢索條件可以知道,它會檢索創建時間(創建事務的 ID )小于當前事務 ID 的行和删除時間(删除事務的 ID )大于當前事務的行,而 id=4 的行上面已經說過,而 id=1 的行由于删除時間(删除事務的 ID )大于當前事務的 ID ,所以 事務2 的 (2) select * from info 也會把 id=1 的數據檢索出來。所以, 事務2中的兩條 select 語句檢索出來的數據都如下:
idname創建版本(事務ID)删除版本(事務ID)1a142b1undefined3c1undefined
UPDATE
InnoDB 執行 UPDATE,實際上是 新插入了一行記錄 ,并保存其創建時間為當前事務的 ID ,同時保存當前事務 ID 到要 UPDATE 的行的删除時間。
start transaction;
update info set name='b' where id=2;
commit;
根據update的更新原則:會生成新的一行,并在原來要修改的列的删除時間列上添加本事務ID,得到表如下:
idname創建版本(事務ID)删除版本(事務ID)1a142b153c1undefined4d3undefined2b5undefined
繼續執行 事務2 的 (2) ,根據 select 語句的檢索條件,得到下表:
idname創建版本(事務ID)删除版本(事務ID)1a142b153c1undefined
還是和 事務2 中 (1) select 得到相同的結果。
❀ 總結:
在 InnoDB 中分為 快照讀 和 當前讀 。快照讀讀的是數據的快照,也就是數據的曆史版本;當前讀就是讀的最新版本的數據,并且在讀的時候加鎖,其他事務都不能對當前行做修改。
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, ∞)
其中區間的右閉即為要加的行鎖,而區間的範圍即是要加的間隙鎖。
五、結語關于髒讀、不可重複讀和幻讀的理解便記錄到這裡了,因筆者水平有限,如有錯誤歡迎指正。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!