1. 程式人生 > >關於mysql可重複讀的原因和幻讀的解決(MVCC-多版本併發控制)

關於mysql可重複讀的原因和幻讀的解決(MVCC-多版本併發控制)

第三個隔離級別RR可以解決不可重複度的問題,那什麼是可重複讀:

Repeatable Read(可重複讀):一個事務在執行過程中可以看到其他事務已經提交的新插入的記錄(讀已經提交的,其實是讀早於本事務開始且已經提交的),但是不能看到其他事務對已有記錄的更新(即晚於本事務開始的),並且,該事務不要求與其他事務是“可序列化”的。

這個定義有兩個方面的意思,一個是不能看到其他事務對已有記錄的更新,另一個是可以看到其他事務已經提交的新插入的記錄。那麼可重複讀的關注點事記錄可重複讀(其他事務對記錄的修改不可見),其他事務的新插入的記錄是可見的(那麼這樣就是會造成幻讀)

直接給出正確的答案,innodb中的MVCC(多版本併發控制)解決了上述的所有問題,對所有!

說說MVCC

一、MVCC簡介

MVCC (Multiversion Concurrency Control),即多版本併發控制技術,它使得大部分支援行鎖的事務引擎,不再單純的使用行鎖來進行資料庫的併發控制,取而代之的是把資料庫的行鎖與行的多個版本結合起來,只需要很小的開銷,就可以實現非鎖定讀,從而大大提高資料庫系統的併發效能

讀鎖:也叫共享鎖、S鎖,若事務T對資料物件A加上S鎖,則事務T可以讀A但不能修改A,其他事務只能再對A加S鎖,而不能加X鎖,直到T釋放A上的S 鎖。這保證了其他事務可以讀A,但在T釋放A上的S鎖之前不能對A做任何修改。

寫鎖:又稱排他鎖、X鎖。若事務T對資料物件A加上X鎖,事務T可以讀A也可以修改A,其他事務不能再對A加任何鎖,直到T釋放A上的鎖。這保證了其他事務在T釋放A上的鎖之前不能再讀取和修改A。

表鎖:操作物件是資料表。Mysql大多數鎖策略都支援(常見mysql innodb),是系統開銷最低但併發性最低的一個鎖策略。事務t對整個表加讀鎖,則其他事務可讀不可寫,若加寫鎖,則其他事務增刪改都不行。

行級鎖:操作物件是資料表中的一行。是MVCC技術用的比較多的,但在MYISAM用不了,行級鎖用mysql的儲存引擎實現而不是mysql伺服器。但行級鎖對系統開銷較大,處理高併發較好。

二、MVCC實現原理

innodb MVCC主要是為Repeatable-Read事務隔離級別做的。在此隔離級別下,A、B客戶端所示的資料相互隔離,互相更新不可見

瞭解innodb的行結構、Read-View的結構對於理解innodb mvcc的實現由重要意義

innodb儲存的最基本row中包含一些額外的儲存資訊 DATA_TRX_ID,DATA_ROLL_PTR,DB_ROW_ID,DELETE BIT

  • 6位元組的DATA_TRX_ID 標記了最新更新這條行記錄的transaction id,每處理一個事務,其值自動+1

  • 7位元組的DATA_ROLL_PTR 指向當前記錄項的rollback segment的undo log記錄,找之前版本的資料就是通過這個指標

  • 6位元組的DB_ROW_ID,當由innodb自動產生聚集索引時,聚集索引包括這個DB_ROW_ID的值,否則聚集索引中不包括這個值.,這個用於索引當中
  • DELETE BIT位用於標識該記錄是否被刪除,這裡的不是真正的刪除資料,而是標誌出來的刪除。真正意義的刪除是在commit的時候

具體的執行過程

begin->用排他鎖鎖定該行->記錄redo log->記錄undo log->修改當前行的值,寫事務編號,回滾指標指向undo log中的修改前的行

上述過程確切地說是描述了UPDATE的事務過程,其實undo log分insert和update undo log,因為insert時,原始的資料並不存在,所以回滾時把insert undo log丟棄即可,而update undo log則必須遵守上述過程

下面分別以select、delete、 insert、 update語句來說明

SELECT

Innodb檢查每行資料,確保他們符合兩個標準:

1、InnoDB只查詢版本早於當前事務版本的資料行(也就是資料行的版本必須小於等於事務的版本),這確保當前事務讀取的行都是事務之前已經存在的,或者是由當前事務建立或修改的行

2、行的刪除操作的版本一定是未定義的或者大於當前事務的版本號,確定了當前事務開始之前,行沒有被刪除

符合了以上兩點則返回查詢結果。

INSERT

InnoDB為每個新增行記錄當前系統版本號作為建立ID。

DELETE

InnoDB為每個刪除行的記錄當前系統版本號作為行的刪除ID。

UPDATE

InnoDB複製了一行。這個新行的版本號使用了系統版本號。它也把系統版本號作為了刪除行的版本。

說明

insert操作時 “建立時間”=DB_ROW_ID,這時,“刪除時間 ”是未定義的;

update時,複製新增行的“建立時間”=DB_ROW_ID,刪除時間未定義,舊資料行“建立時間”不變,刪除時間=該事務的DB_ROW_ID;

delete操作,相應資料行的“建立時間”不變,刪除時間=該事務的DB_ROW_ID;

select操作對兩者都不修改,只讀相應的資料

 

從上面mvcc控制下的select操作需要滿足的條件可以知道我們查詢到的記錄一定是當前事務開始之前就有的,而且不會被更改可見,如果更改之後事務id+1,大餘當前事務id你不滿足條件。因此就可以保證了可重複度,同時也會消除幻讀。

 

網路上絕大多數的人都會認為幻讀的解決是因為間隙鎖。其實不是這樣。

MySQL InnoDB支援三種行鎖定方式:

  • 行鎖(Record Lock):鎖直接加在索引記錄上面。
  • 間隙鎖(Gap Lock):鎖加在不存在的空閒空間,可以是兩個索引記錄之間,也可能是第一個索引記錄之前或最後一個索引之後的空間。
  • Next-Key Lock:行鎖與間隙鎖組合起來用就叫做Next-Key Lock。

預設情況下,InnoDB工作在可重複讀隔離級別下,並且以Next-Key Lock的方式對資料行進行加鎖,這樣可以有效防止幻讀的發生。Next-Key Lock是行鎖與間隙鎖的組合,這樣,當InnoDB掃描索引記錄的時候,會首先對選中的索引記錄加上行鎖(Record Lock),再對索引記錄兩邊的間隙(向左掃描掃到第一個比給定引數小的值, 向右掃描掃描到第一個比給定引數大的值, 然後以此為界,構建一個區間)加上間隙鎖(Gap Lock)如果一個間隙被事務T1加了鎖,其它事務是不能在這個間隙插入記錄的

 

 


舉個例子:
表task_queue
Id           taskId
1              2
3              9
10            20
40            41

開啟一個會話: session 1

sql> set autocommit=0;

   ##

取消自動提交


sql> delete from task_queue where taskId = 20;
sql> insert into task_queue values(20, 20);

在開啟一個會話: session 2

sql> set autocommit=0;

   ##

取消自動提交


sql> delete from task_queue where taskId = 25;
sql> insert into task_queue values(30, 25);

在沒有併發,或是極少併發的情況下, 這樣會可能會正常執行,在Mysql中, 事務最終都是穿行執行, 但是在高併發的情況下, 執行的順序就極有可能發生改變, 變成下面這個樣子:
sql> delete from task_queue where taskId = 20;
sql> delete from task_queue where taskId = 25;
sql> insert into task_queue values(20, 20);
sql> insert into task_queue values(30, 25);

這 個時候最後一條語句:insert into task_queue values(30, 25); 執行時就會爆出死鎖錯誤。因為刪除taskId = 20這條記錄的時候,20 --  41 都被鎖住了, 他們都取得了這一個資料段的共享鎖, 所以在獲取這個資料段的排它鎖時出現死鎖。
 

間隙鎖在InnoDB的唯一作用就是防止其它事務的插入操作,以此來達到防止幻讀的發生,所以間隙鎖不分什麼共享鎖與排它鎖。另外,在上面的例子中,我們選擇的是一個普通(非唯一)索引欄位來測試的,這不是隨便選的,因為如果InnoDB掃描的是一個主鍵、或是一個唯一索引的話,那InnoDB只會採用行鎖方式來加鎖,而不會使用Next-Key Lock的方式,也就是說不會對索引之間的間隙加鎖,仔細想想的話,這個並不難理解,大家也可以自己測試一下。

 

要禁止間隙鎖的話,可以把隔離級別降為讀已提交,或者開啟引數innodb_locks_unsafe_for_binlog

 

很多人都說間隙鎖可以防止幻讀,只不過是在鎖定間隙之後,其他事務不能在這個間隙中發生insert等操作,那其他間隙呢?沒有保證吧?即使是沒有間隙鎖,在這個間隙中插入了新的資料,那事務版本id也大於了當前事務id,是不會被查詢到的,這是mvcc的功勞。