tft每日頭條

 > 生活

 > 幻讀髒讀不可重複讀的現象

幻讀髒讀不可重複讀的現象

生活 更新时间:2024-10-01 03:48:54

幻讀髒讀不可重複讀的現象(幻讀和不可重複讀)1

一、引言

髒讀、不可重複讀和幻讀是數據庫中由于 并發訪問 導緻的數據讀取問題。當多個事務同時進行時可以通過修改數據庫事務的隔離級别來處理這三個問題。

二、問題解釋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串行化,對同一數據的讀寫全加鎖,即對同一數據的讀寫全是互斥了,數據可靠行很強,但是并發性能不忍直視。這種隔離級别雖然解決了上述三個問題,但是犧牲了性能。

總結如下表: √ 代表可能出現,× 代表不會出現。

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

四、MySQL 事務隔離級别的實現

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

原因很簡單,MySQL 默認 Repeatable read 隔離級别,使用了 MVCC 技術,并且解決了幻讀問題。

MVCC

MVCC 全名多版本并發控制,使用它可以保證 InnoDB 存儲引擎下讀操作的一緻性。使用 MVCC 可以查詢被另一個事務修改的行數據,并且可以查看這些行被更新之前的數據,值得注意的是 使用 MVCC 增加了多事務的并發性能,但是并沒有解決幻讀問題 。

1、原理

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

2、實現細節
  • 每行數據都存在一個版本,每次數據更新時都更新該版本。
  • 修改時Copy出當前版本随意修改,各個事務之間互不幹擾。
  • 保存時比較版本号,如果成功(commit),則覆蓋原記錄;失敗則放棄copy(rollback)。

通過上面特點我們可以看出,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;

  • 假設1假設在執行這個事務 ID 為 2 的過程中,剛執行到 (1) ,這時,有另一個事務 ID 為 3 往這個表裡插入了一條數據;

第三個事務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

  • 假設2假設在執行這個事務 ID 為 2 的過程中,剛執行到 (1) ,假設事務執行完 事務3 後,接着又執行了 事務4 ;
第四個事務:

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 的行的删除時間。

  • 假設3假設在執行完 事務2 的 (1) 後又執行,其它用戶執行了事務 3和 4,這時,又有一個用戶對這張表執行了 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 得到相同的結果。

❀ 總結:

  • SELECT讀取創建版本号小于或等于當前事務版本号,并且删除版本号為空或大于當前事務版本号的記錄。如此可以保證在事務在讀取之前記錄是存在的。
  • INSERT将當前事務的版本号保存至插入行的創建版本号。
  • UPDATE新插入一行,并以當前事務的版本号作為新行的創建版本号,同時将原記錄行的删除版本号設置為當前事務版本号。
  • DELETE将當前事務的版本号保存至行的删除版本号。
4、 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, ∞)

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

五、結語

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

,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关生活资讯推荐

热门生活资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved