1. 程式人生 > >MySQL數據庫事務各隔離級別加鎖情況--read committed && MVCC(轉)

MySQL數據庫事務各隔離級別加鎖情況--read committed && MVCC(轉)

釋放 什麽 表空間 版本 read 存儲引擎 extern 不同 重新

本文轉自https://m.imooc.com/article/details?article_id=17290 感謝作者

上篇記錄了我對MySQL 事務 隔離級別read uncommitted的理解。
這篇記錄我對 MySQL 事務隔離級別 read committed & MVCC 的理解。

前言


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

目錄


1.單純加鎖是怎麽實現 read committed 的?
2.真實的演示情況是什麽樣子的?
3.MVCC 實現原理?
4.對於InnoDB 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.記錄的隱藏列
其實有三列

MysqlMVCC是在Innodb存儲引擎中得到支持的,Innodb為每行記錄都實現了三個隱藏字段: 6字節的事務IDDB_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. 將該可見行的值返回。

需要註意的是,新建事務(當前事務)與正在內存中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的生成機制:
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這個名字,提供了讀的非阻塞而已。

MySQL數據庫事務各隔離級別加鎖情況--read committed && MVCC(轉)