天天看點

MySQL InnoDB MVCC原理

轉載自:http://blog.sina.com.cn/s/blog_711b11fd0101bhks.html

一、基礎知識

事務:

事務是一組原子性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

     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“删除”了改行。

MySQL InnoDB MVCC原理

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為1、3、4的行。因為2行被C事務删除了。

例化2:UPDATE

MySQL InnoDB MVCC原理

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隔離級别下

-------于2012.12.23加上start----------------------------------------------

三、深入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事務都是看不到的。當然,這裡還有個小問題要處理一下,就是目前事務自身的變更還是需要看到的。

MySQL InnoDB MVCC原理

在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.          trx_id_t    cr_trx_id,            
  4.          mem_heap_t*          heap)                   
  5. {  
  6.          read_view_t*  view;  
  7.          trx_t*                trx;  
  8.          ulint          n;  
  9.          ut_ad(mutex_own(&kernel_mutex));  
  10.          view = read_view_create_low(UT_LIST_GET_LEN(trx_sys->trx_list), heap);  
  11.          view->creator_trx_id = cr_trx_id;  
  12.          view->type = VIEW_NORMAL;  
  13.          view->undo_no = 0;  
  14.          view->low_limit_no = trx_sys->max_trx_id;  
  15.          view->low_limit_id = view->low_limit_no;  
  16.          n = 0;  
  17.          trx = UT_LIST_GET_FIRST(trx_sys->trx_list);  
  18.          while (trx) {  
  19.                    if (trx->id != cr_trx_id  
  20.                        && (trx->conc_state == TRX_ACTIVE  
  21.                             || trx->conc_state == TRX_PREPARED)) {  
  22.                             read_view_set_nth_trx_id(view, n, trx->id);  
  23.                            n++;  
  24.                             if (view->low_limit_no > trx->no) {  
  25.                                      view->low_limit_no = trx->no;  
  26.                             }  
  27.                    }  
  28.                    trx = UT_LIST_GET_NEXT(trx_list, trx);  
  29.          }  
  30.          view->n_trx_ids = n;  
  31.          if (n > 0) {  
  32.                    view->up_limit_id = read_view_get_nth_trx_id(view, n - 1);  
  33.          } else {  
  34.                    view->up_limit_id = view->low_limit_id;  
  35.          }  
  36.          UT_LIST_ADD_FIRST(view_list, trx_sys->view_list, view);  
  37.          return(view);  
  38. }  

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資訊恢複原始資料狀态