1. 程式人生 > >【mysql】資料庫隔離級別read committed && MVCC

【mysql】資料庫隔離級別read committed && MVCC

前言

可以很負責任的跟大家說,MySQL 中的此隔離級別不單單是通過加鎖實現的,實際上還有repeatable read 隔離級別,其實這兩個隔離級別效果的實現還需要一個輔助,這個輔助就是MVCC-多版本併發控制,但其實它又不是嚴格意義上的多版本併發控制,是不是很懵,沒關係,我們一一剖析。

1.單純加鎖是怎麼實現 read committed 的?

從此隔離級別效果入手:事務只能讀其他事務已提交的的記錄。 資料庫事務隔離級別的實現,InnoDB 支援行級鎖,寫時加的是行級排他鎖(X lock),那麼當其他事務訪問另一個事務正在update (除select操作外其他操作本質上都是寫操作)的同一條記錄時,事務的讀操作會被阻塞。所以只能等到記錄(其實是索引上的鎖

)上的排他鎖釋放後才能進行訪問,也就是事務提交的時候。這樣確實能實現read commited隔離級別效果。 資料庫這樣做確實可以實現 事務只能讀其他事務已提交的的記錄 的效果,但是這是很低效的一種做法,為什麼呢?因為對於大部分應用來說,讀操作是多於寫操作的,當寫操作加鎖時,那麼讀操作全部被阻塞,這樣會導致應用的相應能力受資料庫的牽制。

2.真實的演示情況是什麼樣子的?

看如下操作: 1.開啟兩個客戶端例項,設定事務隔離級別為read committed,並各自開啟事務。

 set transaction isolation level read committed;
 set autocommit = 0;
 begin;

2.客戶端1做更新操作:

update test set name = '測試' where id =32;

結果如下圖所示:

3.客戶端2做查詢操作:

select name from test where id = 32;

結果如下所示:

這時估計你有疑問了,正在 被客戶端1 upate 的記錄,客戶端2還能無阻塞的讀到,而且讀到的是未更改之前的資料。 那就是 InnoDB 的輔助打得好,因為內部使用了 MVCC 機制,實現了一致性非阻塞讀,大大提高了併發讀寫效率,寫不影響讀,且讀到的事記錄的映象版本。

下面開始介紹 MVCC 原理。

3.MVCC 實現原理

網上對 MVCC 實現原理 的講述五花八門,良莠不齊。 包括《高效能MySQL》對 MVCC 的講解只是停留在表象,並沒有結合原始碼去分析。當然絕大多數人還是相信這本書的,從來沒有進行深剖,思考。 如下是 《高效能MySQL》對 MVCC實現原理 的描述:

"InnoDB的 MVCC ,是通過在每行記錄的後面儲存兩個隱藏的列來實現的。這兩個列,
一個儲存了行的建立時間,一個儲存了行的過期時間,
當然儲存的並不是實際的時間值,而是系統版本號。"

就是這本書,矇蔽了真理,害人不淺。

我們還是看原始碼吧:

1.記錄的隱藏列 其實有三列

在Mysql中MVCC是在Innodb儲存引擎中得到支援的,Innodb為每行記錄都實現了三個隱藏欄位:

6位元組的事務ID(DB_TRX_ID)

7位元組的回滾指標(DB_ROLL_PTR)

隱藏的ID

6位元組的事物ID用來標識該行所述的事務,7位元組的回滾指標需要了解下Innodb的事務模型。

2.MVCC 實現的依賴項 MVCC 在mysql 中的實現依賴的是 undo log 與 read view。

1.undo log: undo log中記錄的是資料表記錄行的多個版本,也就是事務執行過程中的回滾段,其實就是MVCC 中的一行原始資料的多個版本映象資料。
2.read view: 主要用來判斷當前版本資料的可見性。

3.undo log

undo log是為回滾而用,具體內容就是copy事務前的資料庫內容(行)到undo buffer,在適合的時間把undo buffer中的內容重新整理到磁碟。undo buffer與redo buffer一樣,也是環形緩衝,但當緩衝滿的時候,undo buffer中的內容會也會被重新整理到磁碟;與redo log不同的是,磁碟上不存在單獨的undo log檔案,所有的undo log均存放在主ibd資料檔案中(表空間),即使客戶端設定了每表一個數據檔案也是如此。

我們通過行的更新過程來看下undo log 是如何形成的?

3.1 行的更新過程 下面演示下事務對某行記錄的更新過程:

  1. 初始資料行

     

     

    F1~F6是某行列的名字,1~6是其對應的資料。後面三個隱含欄位分別對應該行的事務號和回滾指標,假如這條資料是剛INSERT的,可以認為ID為1,其他兩個欄位為空。 2.事務1更改該行的各欄位的值

     

    當事務1更改該行的值時,會進行如下操作: 用排他鎖鎖定該行

    記錄redo log 把該行修改前的值Copy到undo log,即上圖中下面的行 修改當前行的值,填寫事務編號,使回滾指標指向undo log中的修改前的行 3.事務2修改該行的值

     

    與事務1相同,此時undo log,中有有兩行記錄,並且通過回滾指標連在一起。

4.read view 判斷當前版本資料項是否可見

在innodb中,建立一個新事務的時候,innodb會將當前系統中的活躍事務列表(trx_sys->trx_list)建立一個副本(read view),副本中儲存的是系統當前不應該被本事務看到的其他事務id列表。當用戶在這個事務中要讀取該行記錄的時候,innodb會將該行當前的版本號與該read view進行比較。 具體的演算法如下:

  1. 設該行的當前事務id為trx_id_0,read view中最早的事務id為trx_id_1, 最遲的事務id為trx_id_2。
  2. 如果trx_id_0< trx_id_1的話,那麼表明該行記錄所在的事務已經在本次新事務建立之前就提交了,所以該行記錄的當前值是可見的。跳到步驟6.
  3. 如果trx_id_0>trx_id_2的話,那麼表明該行記錄所在的事務在本次新事務建立之後才開啟,所以該行記錄的當前值不可見.跳到步驟5。
  4. 如果trx_id_1<=trx_id_0<=trx_id_2, 那麼表明該行記錄所在事務在本次新事務建立的時候處於活動狀態,從trx_id_1到trx_id_2進行遍歷,如果trx_id_0等於他們之中的某個事務id的話,那麼不可見。跳到步驟5.
  5. 從該行記錄的DB_ROLL_PTR指標所指向的回滾段中取出最新的undo-log的版本號,將它賦值該trx_id_0,然後跳到步驟2.
  6. 將該可見行的值返回。

總的來說:

   記錄的DATA_TRX_ID < view->up_limit_id:在建立read view時,修改該記錄的事務已提交,該記錄可見

   DATA_TRX_ID >= view->low_limit_id:當前事務啟動後被修改,該記錄不可見

   DATA_TRX_ID 位於(view->up_limit_id,view->low_limit_id):需要在活躍讀寫事務陣列查詢trx_id是否存在,如果存在,記錄對於當前read view是不可見的,如果不存再,說明是當前本事務更新了這條記錄,所以是可見的。

需要注意的是,新建事務(當前事務)與正在記憶體中commit 的事務不在活躍事務連結串列中。

對應程式碼如下:

函式:read_view_sees_trx_id。 read_view中儲存了當前全域性的事務的範圍: 【low_limit_id, up_limit_id】1. 當行記錄的事務ID小於當前系統的最小活動id,就是可見的。   if (trx_id < view->up_limit_id) {     return(TRUE);   }2. 當行記錄的事務ID大於當前系統的最大活動id,就是不可見的。   if (trx_id >= view->low_limit_id) {     return(FALSE);   }3. 當行記錄的事務ID在活動範圍之中時,判斷是否在活動連結串列中,如果在就不可見,如果不在就是可見的。   for (i = 0; i < n_ids; i++) {     trx_id_t view_trx_id       = read_view_get_nth_trx_id(view, n_ids - i - 1);     if (trx_id <= view_trx_id) {     return(trx_id != view_trx_id);     }   }

5 事務隔離級別的影響

但是:對於兩張不同的事務隔離級別   tx_isolation='READ-COMMITTED': 語句級別的一致性:只要當前語句執行前已經提交的資料都是可見的。   tx_isolation='REPEATABLE-READ'; 語句級別的一致性:只要是當前事務執行前已經提交的資料都是可見的。 針對這兩張事務的隔離級別,使用相同的可見性判斷邏輯是如何做到不同的可見性的呢?

6.不同隔離級別下read view的生成原則

 read view是和SQL語句繫結的,在每個SQL語句執行前申請或獲取(RR隔離級別:事務第一個select申請,之後都用這個;RC隔離級別:每個select都會申請)

這裡就要看看read_view的生成機制:1. read-commited:   函式:ha_innobase::external_lock   if (trx->isolation_level <= TRX_ISO_READ_COMMITTED     && trx->global_read_view) {     /* At low transaction isolation levels we let     each consistent read set its own snapshot */   read_view_close_for_mysql(trx); 即:在每次語句執行的過程中,都關閉read_view, 重新在row_search_for_mysql函式中建立當前的一份read_view。 這樣就可以根據當前的全域性事務連結串列建立read_view的事務區間,實現read committed隔離級別。2. repeatable read:   在repeatable read的隔離級別下,建立事務trx結構的時候,就生成了當前的global read view。

使用trx_assign_read_view函式建立,一直維持到事務結束,這樣就實現了repeatable read隔離級別。

正是因為6中的read view 生成原則,導致在不同隔離級別()下,read committed 總是讀最新一份快照資料,而repeatable read 讀事務開始時的行資料版本。

4.InnoDB MVCC 實現原理的深刻反思

上述更新前建立undo log,根據各種策略讀取時非阻塞就是MVCC,undo log中的行就是MVCC中的多版本,這個可能與我們所理解的MVCC有較大的出入。

一般我們認為MVCC有下面幾個特點:

每行資料都存在一個版本,每次資料更新時都更新該版本 修改時Copy出當前版本隨意修改,個事務之間無干擾 儲存時比較版本號,如果成功(commit),則覆蓋原記錄;失敗則放棄copy(rollback) 就是每行都有版本號,儲存時根據版本號決定是否成功,聽起來含有樂觀鎖的味道。。。,而

Innodb的實現方式是:

事務以排他鎖的形式修改原始資料 把修改前的資料存放於undo log,通過回滾指標與主資料關聯 修改成功(commit)啥都不做,失敗則恢復undo log中的資料(rollback)

二者最本質的區別是,當修改資料時是否要排他鎖定,如果鎖定了還算不算是MVCC?

Innodb的實現真算不上MVCC,因為並沒有實現核心的多版本共存,undo log中的內容只是序列化的結果,記錄了多個事務的過程,不屬於多版本共存。但理想的MVCC是難以實現的,當事務僅修改一行記錄使用理想的MVCC模式 是沒有問題的,可以通過比較版本號進行回滾;但當事務影響到多行資料時,理想的MVCC據無能為力了。

比如,如果Transaciton1執行理想的MVCC,修改Row1成功,而修改Row2失敗,此時需要回滾Row1,但因為Row1沒有被 鎖定,其資料可能又被Transaction2所修改,如果此時回滾Row1的內容,則會破壞Transaction2的修改結果,導致 Transaction2違反ACID。

理想MVCC難以實現的根本原因在於企圖通過樂觀鎖代替二段提交。修改兩行資料,但為了保證其一致性,與修改兩個分散式系統中的資料並無區別, 而二提交是目前這種場景保證一致性的唯一手段。二段提交的本質是鎖定,樂觀鎖的本質是消除鎖定,二者矛盾,故理想的MVCC難以真正在實際中被應 用,Innodb只是借了MVCC這個名字,提供了讀的非阻塞而已。

轉載連結:https://www.jianshu.com/p/fd51cb8dc03b