【mysql】資料庫隔離級別read committed && MVCC
前言
可以很負責任的跟大家說,MySQL 中的此隔離級別不單單是通過加鎖實現的,實際上還有repeatable read 隔離級別,其實這兩個隔離級別效果的實現還需要一個輔助,這個輔助就是MVCC-多版本併發控制,但其實它又不是嚴格意義上的多版本併發控制,是不是很懵,沒關係,我們一一剖析。
1.單純加鎖是怎麼實現 read committed 的?
從此隔離級別效果入手:事務只能讀其他事務已提交的的記錄。 資料庫事務隔離級別的實現,InnoDB 支援行級鎖,寫時加的是行級排他鎖(X lock),那麼當其他事務訪問另一個事務正在update (除select操作外其他操作本質上都是寫操作)的同一條記錄時,事務的讀操作會被阻塞。所以只能等到記錄(其實是索引上的鎖
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 行的更新過程 下面演示下事務對某行記錄的更新過程:
-
初始資料行
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進行比較。 具體的演算法如下:
- 設該行的當前事務id為trx_id_0,read view中最早的事務id為trx_id_1, 最遲的事務id為trx_id_2。
- 如果trx_id_0< trx_id_1的話,那麼表明該行記錄所在的事務已經在本次新事務建立之前就提交了,所以該行記錄的當前值是可見的。跳到步驟6.
- 如果trx_id_0>trx_id_2的話,那麼表明該行記錄所在的事務在本次新事務建立之後才開啟,所以該行記錄的當前值不可見.跳到步驟5。
- 如果trx_id_1<=trx_id_0<=trx_id_2, 那麼表明該行記錄所在事務在本次新事務建立的時候處於活動狀態,從trx_id_1到trx_id_2進行遍歷,如果trx_id_0等於他們之中的某個事務id的話,那麼不可見。跳到步驟5.
- 從該行記錄的DB_ROLL_PTR指標所指向的回滾段中取出最新的undo-log的版本號,將它賦值該trx_id_0,然後跳到步驟2.
- 將該可見行的值返回。
總的來說:
記錄的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