MVCC簡介


1. MVCC簡介

1.1 什么是MVCC

MVCC是一種多版本並發控制機制。

1.2 MVCC是為了解決什么問題?

  • 大多數的MYSQL事務型存儲引擎,如,InnoDB,Falcon以及PBXT都不使用一種簡單的行鎖機制.事實上,他們都和MVCC–多版本並發控制來一起使用.
  • 大家都應該知道,鎖機制可以控制並發操作,但是其系統開銷較大,而MVCC可以在大多數情況下代替行級鎖,使用MVCC,能降低其系統開銷.

1.3 MVCC實現

MVCC是通過保存數據在某個時間點的快照來實現的. 不同存儲引擎的MVCC. 不同存儲引擎的MVCC實現是不同的,典型的有樂觀並發控制和悲觀並發控制.

2.MVCC 具體實現分析

下面,我們通過InnoDB的MVCC實現來分析MVCC使怎樣進行並發控制的. 
InnoDB的MVCC,是通過在每行記錄后面保存兩個隱藏的列來實現的,這兩個列,分別保存了這個行的創建時間,一個保存的是行的刪除時間。這里存儲的並不是實際的時間值,而是系統版本號(可以理解為事務的ID),沒開始一個新的事務,系統版本號就會自動遞增,事務開始時刻的系統版本號會作為事務的ID.下面看一下在REPEATABLE READ隔離級別下,MVCC具體是如何操作的.

2.1簡單的小例子

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

假設系統的版本號從1開始.

INSERT

InnoDB為新插入的每一行保存當前系統版本號作為版本號. 
第一個事務ID為1;

start transaction; insert into yang values(NULL,'yang') ; insert into yang values(NULL,'long'); insert into yang values(NULL,'fei'); commit;
  • 1
  • 2
  • 3
  • 4
  • 5

對應在數據中的表如下(后面兩列是隱藏列,我們通過查詢語句並看不到)

id name 創建時間(事務ID) 刪除時間(事務ID)
1 yang 1 undefined
2 long 1 undefined
3 fei 1 undefined

SELECT

InnoDB會根據以下兩個條件檢查每行記錄: 
a.InnoDB只會查找版本早於當前事務版本的數據行(也就是,行的系統版本號小於或等於事務的系統版本號),這樣可以確保事務讀取的行,要么是在事務開始前已經存在的,要么是事務自身插入或者修改過的. 
b.行的刪除版本要么未定義,要么大於當前事務版本號,這可以確保事務讀取到的行,在事務開始之前未被刪除. 
只有a,b同時滿足的記錄,才能返回作為查詢結果.

DELETE

InnoDB會為刪除的每一行保存當前系統的版本號(事務的ID)作為刪除標識. 
看下面的具體例子分析: 
第二個事務,ID為2;

start transaction; select * from yang; //(1) select * from yang; //(2) commit; 
  • 1
  • 2
  • 3
  • 4

假設1

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

start transaction; insert into yang values(NULL,'tian'); commit;
  • 1
  • 2
  • 3

這時表中的數據如下:

id name 創建時間(事務ID) 刪除時間(事務ID)
1 yang 1 undefined
2 long 1 undefined
3 fei 1 undefined
4 tian 3 undefined

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

id name 創建時間(事務ID) 刪除時間(事務ID)
1 yang 1 undefined
2 long 1 undefined
3 fei 1 undefined

假設2

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

start transaction; delete from yang where id=1; commit; 
  • 1
  • 2
  • 3

此時數據庫中的表如下:

id name 創建時間(事務ID) 刪除時間(事務ID)
1 yang 1 4
2 long 1 undefined
3 fei 1 undefined
4 tian 3 undefined

接着執行事務ID為2的事務(2),根據SELECT 檢索條件可以知道,它會檢索創建時間(創建事務的ID)小於當前事務ID的行和刪除時間(刪除事務的ID)大於當前事務的行,而id=4的行上面已經說過,而id=1的行由於刪除時間(刪除事務的ID)大於當前事務的ID,所以事務2的(2)select * from yang也會把id=1的數據檢索出來.所以,事務2中的兩條select 語句檢索出來的數據都如下:

id name 創建時間(事務ID) 刪除時間(事務ID)
1 yang 1 4
2 long 1 undefined
3 fei 1 undefined

UPDATE

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

假設3

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

start transaction; update yang set name='Long' where id=2; commit;
  • 1
  • 2
  • 3

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

id name 創建時間(事務ID) 刪除時間(事務ID)
1 yang 1 4
2 long 1 5
3 fei 1 undefined
4 tian 3 undefined
2 Long 5 undefined

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

id name 創建時間(事務ID) 刪除時間(事務ID)
1 yang 1 4
2 long 1 5
3 fei 1 undefined

還是和事務2中(1)select 得到相同的結果.

 

###2:

MySQL InnoDB存儲引擎,實現的是基於多版本的並發控制協議——MVCC (Multi-Version Concurrency Control) (注:與MVCC相對的,是基於鎖的並發控制,Lock-Based Concurrency Control)。MVCC最大的好處,相信也是耳熟能詳:讀不加鎖,讀寫不沖突。在讀多寫少的OLTP應用中,讀寫不沖突是非常重要的,極大的增加了系統的並發性能。

InnoDB在每行數據都增加兩個隱藏字段,一個記錄創建的版本號,一個記錄刪除的版本號。

* SELECT:
當隔離級別是REPEATABLE READ時select操作,InnoDB必須每行數據來保證它符合兩個條件:
1、InnoDB必須找到一個行的版本,它至少要和事務的版本一樣老(也即它的版本號不大於事務的版本號)。這保證了不管是事務開始之前,或者事務創建時,或者修改了這行數據的時候,這行數據是存在的。
2、這行數據的刪除版本必須是未定義的或者比事務版本要大。這可以保證在事務開始之前這行數據沒有被刪除。
符合這兩個條件的行可能會被當作查詢結果而返回。
* INSERT:<br>InnoDB為這個新行記錄當前的系統版本號。
* DELETE:<br>InnoDB將當前的系統版本號設置為這一行的刪除ID。
* UPDATE:<br>InnoDB會寫一個這行數據的新拷貝,這個拷貝的版本為當前的系統版本號。它同時也會將這個版本號寫到舊行的刪除版本里。
這種額外的記錄所帶來的結果就是對於大多數查詢來說根本就不需要獲得一個鎖。他們只是簡單地以最快的速度來讀取數據,確保只選擇符合條件的行。這個方案的缺點在於存儲引擎必須為每一行存儲更多的數據,<br>做更多的檢查工作,處理更多的善后操作
MVCC只工作在REPEATABLE READ和READ COMMITED隔離級別下。READ UNCOMMITED不是MVCC兼容的,因為查詢不能找到適合他們事務版本的行版本;它們每次都只能讀到最新的版本。<br>SERIABLABLE也不與MVCC兼容,因為讀操作會鎖定他們返回的每一行數據。

##3:

一、基礎知識

 事務: 事務是一組原子性sql查詢語句,被當作一個工作單元。若mysql對改事務單元內的所有sql語句都正常的執行完,則事務操作視為成功,所有的sql語句才對數據生效,若sql中任意不能執行或出錯則事務操作失敗,所有對數據的操作則無效(通過回滾恢復數據)。事務有四個屬性:

1、原子性:事務被認為不可分的一個工作單元,要么全部正常執行,要么全部不執行。

2、一致性:事務操作對數據庫總是從一種一致性的狀態轉換成另外一種一致性狀態。

3、隔離性:一個事務的操作結果在內部一致,可見,而對除自己以外的事務是不可見的。

4、永久性:事務在未提交前數據一般情況下可以回滾恢復數據,一旦提交(commit)數據的改變則變成永久(當然用update肯定還能修改)。

ps:MYSAM 引擎的數據庫不支持事務,所以事務最好不要對混合引擎(如INNODB 、MYISAM)操作,若能正常運行且是你想要的最好,否則事務中對非支持事務表的操作是不能回滾恢復的。

 

讀鎖:也叫共享鎖、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: 多版本並發控制(MVCC,Multiversion Currency Control)。一般情況下,事務性儲存引擎不是只使用表鎖,行加鎖的處理數據,而是結合了MVCC機制,以處理更多的並發問題。Mvcc處理高並發能力最強,但系統開銷比最大(較表鎖、行級鎖),這是最求高並發付出的代價。

 

Autocommit: mysql一個系統變量,默認情況下autocommit=1表示mysql把沒一條sql語句自動的提交,而不用commit語句。所以,當要開啟事務操作時,要把autocommit設為0,可以通過“set session autocommit=0; ”來設置

 

二、MVCC實現原理以及例化理解

第一:先看看網絡上幾乎全部一樣的理解,包括《高性能mysql第二版(中文版)》也如此說明,這樣是很容易理解。但筆者覺得2個地方不妥,先看內容,在后面筆者會給出不妥地方用(1、2…)加粗標志出來,且給出測試證明。

Ps:這些只是外部看來的理解層面,深層次在第三點講解

------------------------------------------

InnoDB實現MVCC的方法是,它存儲了每一行的兩個(1)額外的隱藏字段,這兩個隱藏字段分別記錄了行的創建的時間和刪除的時間。在每個事件發生的時候,每行存儲版本號,而不是存儲事件實際發生的時間。每次事物的開始這個版本號都會增加。自記錄時間開始,每個事物都會保存記錄的系統版本號。依照事物的 版本來檢查每行的版本號。在事物隔離級別為可重復讀的情況下,來看看怎樣應用它。

SELECT

Innodb檢查沒行數據,確保他們符合兩個標准:

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

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

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

  INSERT   (3)   

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

  DELETE

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

  UPDATE

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

----------------------------------------------

(1)    不是兩個,是三個。

1DB_TRX_ID:一個6byte的標識,每處理一個事務,其值自動+1,上述說到的“創建時間”和“刪除時間”記錄的就是這個DB_TRX_ID的值,如insert、update、delete操作時,刪除操作用1個bit表示。 DB_TRX_ID是最重要的一個,可以通過語句“show engine innodb status”來查找,如下:

   -----------------------------------------

   ……

      TRANSACTIONS

------------

Trx id counter 0 430621

Purge done for trx's n:o < 0 430136 undo n:o < 0 0

History list length 7

……

   ------------------------------------------

2DB_ROLL_PTR: 大小是7byte,指向寫到rollback segment(回滾段)的一條undo log記錄(update操作的話,記錄update前的ROW值)

3DB_ROW_ID: 大小是6byte,該值隨新行插入單調增加,當由innodb自動產生聚集索引時,聚集索引包括這個DB_ROW_ID的值,不然的話聚集索引中不包括這個值. 這個用於索引當中

(2)    這里的不是真正的刪除數據,而是標志出來的刪除。真正意義的刪除是在commit的時候。網上的說法很容易讓讀者誤解

(3)    在insert操作時 “創建時間”=DB_ROW_ID,這時,“刪除時間 ”是未定義的;在update時,復制新增行的“創建時間”=DB_ROW_ID,刪除時間未定義,舊數據行“創建時間”不變,刪除時間=該事務的DB_ROW_ID;delete操作,相應數據行的“創建時間”不變,刪除時間=該事務的DB_ROW_ID;select操作對兩者都不修改,只讀相應的數據

 

第二、下面用圖形化形式表示MVCC如何處理select、insert、delete、update

有兩個事務A、B

假設開始時間順序ABCD,且DB_TRX_ID滿足以下情況

A. DB_TRX_ID = 2010

B. DB_TRX_ID = 2011

C. DB_TRX_ID = 2012

D. DB_TRX_ID = 2013

注意:

1、B. DB_TRX_ID> A. DB_TRX_ID是因為DB_TRX_ID的值是系統版本號的值,系統版本號是自動增加的,所以DB_TRX_ID也是自動增加。但是會出現這種情況,假如A事務開始后B事務開始前有一個insert操作插入一行數據(沒有bengin、comint),則B. DB_TRX_ID= A. DB_TRX_ID+1+1 ,並不符合不是說系統版本號增量為1,其實並不矛盾,其實每一條sql操作可以當作一個事務,因為autocommit=1,所以這個insert操作是一個事務,A事務之后新增2個事務, 所以是加2而不是1。

2、下面例化圖只是筆者方便大家理解而設計的圖片,紅色框代表隱藏兩列

例化1:SECLET

這是表test數據

trx代表改行數據是那個事務創建

creat_num是“創建時間”,也就是DB_TRX_ID值

dele_num是“刪除時間 ”,空列代表沒被任何事務標志為已“刪除”,圖中id為2的數據行的dele_num=2012表示事務C“刪除”了改行。

 

B事務有select * from test;語句,按照MVCC原理,該語句相當於:select * from test where creat_num>=2011 and (dele_num=NULL OR dele_num>2011),所以返回數據是id為1、2行。

D事務select * from test;則返回出id為2的行。因為2行被C事務刪除了。

例化2:UPDATE

 

A事務一條語句“update from test set col=’winben’ where col=’benwin’”。

則先復制一條數據如藍色框,creat_num=DB_TRX_ID(這里是2010),dele_num=NULL,然后把舊行數據的設dele_num=2010,等commit后則刪除舊數據行

例化3:DELET

刪除就是設dele_num= DB_TRX_ID

-------於2012.12.23加上start----------------------------------------------------------------

和一位淘寶網友討論一個問題(關於事務隔離級別,這里就直接貼不整理了)

網友: 請教個問題,innodb的事務,一定是按ID順序提交么? ID為101的一定在ID為100的事務之后?

筆者:這個問題我也不確定。我認為不是按順序的,可以這樣想一下,加入a事務很大是id100,然后還沒commit之前有id為101的事務b並發開始處理,但b事務很小處理完了,如果要等a事務的話則是一個雞肋了。當然還有考慮鎖的問題,如果a事務設置了排他鎖,且b事務有寫操作那不事務則在等待隊列中了,那commit的順序肯定是a然后b的!

網友:如果是這樣的話,假設有100,101,102三個事務,101最先提交了,這時新事務103,應該能看到101的更改,而如果按當前活躍ID的最小的比較(這時為100),那就看不到101的更新。

筆者:結合事務隔離級別:

1、READ UNCOMMITTED ,不適用MVCC讀,可以讀到其他事務修改甚至未提交的

2、READ COMMITTED ,其他事務對數據庫的修改,只要已經提交,其修改的結果就是可見的,與這兩個事務開始的先后順序無關,不完全適用於MVCC讀,

像你說的100的讀101的是可以的(按照MVCC理論應該不行的),但適用102讀101(能套MVCC理論)。

3、REPEATABLE READ,可重復讀,完全適用MVCC,只能讀取在它開始之前已經提交的事務對數據庫的修改,在它開始以后,所有其他事務對數據庫的修改對它來說均不可見

4、 SERIALIZABLE ,完全不適合適用MVCC,這樣所有的query都會加鎖,再它之后的事務都要等待

MVCC只工作在REPEATABLE READ和READ COMMITED隔離級別下

 

 

三、深入MVCC實現機制

1、到這里很多人就會發現,如果確實根據creat_num 即時事務DB_TRX_ID去比較獲取事務的話,按道理在一個事務B(比A后,但A還沒commit)select的話B. DB_TRX_ID>A.DB_TRX_ID則應該能返回A事務對數據的操作以及修改。那不是和前面矛盾?其實不然,只是前面沒有講到以下內容。

InnoDB每個事務在開始的時候,會將當前系統中的活躍事務列表(trx_sys->trx_list)創建一個副本(read view),然后一致性讀去比較記錄的tx id的時候,並不是根據當前事務的tx id,而是根據read view最早一個事務的tx id(read view->up_limit_id)來做比較的,這樣就能確保在事務B之前沒有提交的所有事務的變更,B事務都是看不到的。當然,這里還有個小問題要處理一下,就是當前事務自身的變更還是需要看到的。

 

在storage/innobase/read/read0read.c中實現了創建read view的函數read_view_open_now,在storage/innobase/include/read0read.ic中實現了判斷一致性讀是否可見的read_view_sees_trx_id

代碼:

  1. read_view_t*  
  2. read_view_open_now(  
  3. /*===============*/  
  4.          trx_id_t    cr_trx_id,          /*!< in: trx_id of creating 
  5.                                               transaction, or 0 used in purge */  
  6.          mem_heap_t*          heap)                 /*!< in: memory heap from which 
  7.                                                allocated */  
  8. {  
  9.          read_view_t*  view;  
  10.          trx_t*                trx;  
  11.          ulint          n;  
  12.          ut_ad(mutex_own(&kernel_mutex));  
  13.          view = read_view_create_low(UT_LIST_GET_LEN(trx_sys->trx_list), heap);  
  14.          view->creator_trx_id = cr_trx_id;  
  15.          view->type = VIEW_NORMAL;  
  16.          view->undo_no = 0;  
  17.          /* No future transactions should be visible in the view */  
  18.          view->low_limit_no = trx_sys->max_trx_id;  
  19.          view->low_limit_id = view->low_limit_no;  
  20.          n = 0;  
  21.          trx = UT_LIST_GET_FIRST(trx_sys->trx_list);  
  22.          /* No active transaction should be visible, except cr_trx */  
  23.          while (trx) {  
  24.                    if (trx->id != cr_trx_id  
  25.                        && (trx->conc_state == TRX_ACTIVE  
  26.                             || trx->conc_state == TRX_PREPARED)) {  
  27.                             read_view_set_nth_trx_id(view, n, trx->id);  
  28.                            n++;  
  29.                             /* NOTE that a transaction whose trx number is < 
  30.                             trx_sys->max_trx_id can still be active, if it is 
  31.                             in the middle of its commit! Note that when a 
  32.                             transaction starts, we initialize trx->no to 
  33.                             IB_ULONGLONG_MAX. */  
  34.                             if (view->low_limit_no > trx->no) {  
  35.                                      view->low_limit_no = trx->no;  
  36.                             }  
  37.                    }  
  38.                    trx = UT_LIST_GET_NEXT(trx_list, trx);  
  39.          }  
  40.          view->n_trx_ids = n;  
  41.          if (n > 0) {  
  42.                    /* The last active transaction has the smallest id: */  
  43.                    view->up_limit_id = read_view_get_nth_trx_id(view, n - 1);  
  44.          } else {  
  45.                    view->up_limit_id = view->low_limit_id;  
  46.          }  
  47.          UT_LIST_ADD_FIRST(view_list, trx_sys->view_list, view);  
  48.          return(view);  
  49.   
  50. }  

 

2、MVCC如何控制update操作

前面說先復制新數據,並插入DB_TRX_ID的值,在把舊數據的刪除標志DB_TRX_ID

現在先介紹幾個概念:

DB_ROLL_PTR是指向回滾段中舊版本7byte回滾指針。

redo log:重做日志,就是每次mysql在執行寫入數據前先把要寫的信息保存在重寫日志中,但出現斷電,奔潰,重啟等等導致數據不能正常寫入期望數據時,服務器可以通過redo_log中的信息重新寫入數據。

undo log:撤銷日志,與redo log恰恰相反,當一些更改在執行一半時,發生意外,而無法完成,則可以根據撤消日志恢復到更改之前的壯態。

mvcc中update步驟:

1、 記錄事務中修改行數據的相應字段和值(包括舊版本事務id)在undo-log中記錄。

2、 修改相應數據。

3、 在redo-log中保存要修改的相應(新版本事務id)數據寫入

以上驟詳細代碼內容可看:

http://hi.baidu.com/gao1738/blog/item/dcec39d6185af2049d163d8c.html

4、 假如update不能正常運行怎根據undo-log redo-log 來回復

5、 當然如果當前版本事務沒有commit的話則通過undo-log信息恢復原始數據狀態.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM