1. 程式人生 > 程式設計 >一篇文章帶你掌握mysql的一致性檢視(MVCC)

一篇文章帶你掌握mysql的一致性檢視(MVCC)

提到事務,你肯定會想到ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔離性、永續性),我們就來說說其中I,也就是“隔離性”。

當資料庫上有多個事務同時執行的時候,就可能出現髒讀(dirty read)、不可重複讀(non-repeatable read)、幻讀(phantom read)的問題,所以下面我們來說說隔離級別。

SQL標準的事務隔離級別包括:讀未提交(read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)、序列化(serializable)。

  • 讀未提交是指,一個事務還沒提交時,它做的變更就能被別的事務看到。
  • 讀提交指,一個事務提交之後,它做的變更才會被其他事務看到。
  • 可重複讀指,一個事務執行過程中看到的資料,總是跟這個事務在啟動時看到的資料時一致的。當然可重複讀隔離級別下,未提交變更對其他事務也是不可見的。
  • 序列化,顧名思義是對於同一行記錄,“寫”會加“寫鎖”,“讀”會加“讀鎖”。當出現讀寫鎖衝突的時候,後訪問的事務必須等前一個事務執行完成,才能繼續執行。

MySQL中支援的四種隔離級別

MySQL雖然支援4種隔離級別,但與SQL標準中所規定的各級隔離級別允許發生的問題卻有些出入,MySQL在REPEATABLE READ隔離級別下,是可以禁止幻讀問題的發生的。

我們可以通過: SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;

來設定隔離級別。

其中的level可選值有4個:

level: {
     REPEATABLE READ
   | READ COMMITTED
   | READ UNCOMMITTED
   | SERIALIZABLE
}
複製程式碼

MVCC原理

對於使用InnoDB儲存引擎的表來說,它的聚簇索引記錄中都包含必要的隱藏列:

  • trx_id:每次一個事務對某條聚簇索引記錄進行改動時,都會把該事務的事務id賦值給trx_id隱藏列。

ReadView

ReadView所解決的問題是使用READ COMMITTED和REPEATABLE READ隔離級別的事務中,不能讀到未提交的記錄,這需要判斷一下版本鏈中的哪個版本是當前事務可見的。

ReadView中主要包含4個比較重要的內容:

  • m_ids:表示在生成ReadView時當前系統中活躍的讀寫事務的事務id列表。
  • min_trx_id:表示在生成ReadView時當前系統中活躍的讀寫事務中最小的事務id,也就是m_ids中的最小值。
  • max_trx_id:表示生成ReadView時系統中應該分配給下一個事務的id值。
  • creator_trx_id:表示生成該ReadView的事務的事務id。

ReadView是如何工作的?

有了這些資訊,這樣在訪問某條記錄時,只需要按照下邊的步驟判斷記錄的某個版本是否可見:

  • 如果被訪問版本的trx_id屬性值與ReadView中的creator_trx_id值相同,意味著當前事務在訪問它自己修改過的記錄,所以該版本可以被當前事務訪問。
  • 如果被訪問版本的trx_id屬性值小於ReadView中的min_trx_id值,表明生成該版本的事務在當前事務生成ReadView前已經提交,所以該版本可以被當前事務訪問。
  • 如果被訪問版本的trx_id屬性值大於ReadView中的max_trx_id值,表明生成該版本的事務在當前事務生成ReadView後才開啟,所以該版本不可以被當前事務訪問。
  • 如果被訪問版本的trx_id屬性值在ReadView的min_trx_id和max_trx_id之間,那就需要判斷一下trx_id屬性值是不是在m_ids列表中,如果在,說明建立ReadView時生成該版本的事務還是活躍的,該版本不可以被訪問;如果不在,說明建立ReadView時生成該版本的事務已經被提交,該版本可以被訪問。

如果某個版本的資料對當前事務不可見的話,那就順著版本鏈找到下一個版本的資料,繼續按照上邊的步驟判斷可見性,依此類推,直到版本鏈中的最後一個版本。如果最後一個版本也不可見的話,那麼就意味著該條記錄對該事務完全不可見,查詢結果就不包含該記錄。

在MySQL中,READ COMMITTED和REPEATABLE READ隔離級別的的一個非常大的區別就是它們生成ReadView的時機不同。

我們這裡使用一個示例來解釋:

mysql> CREATE TABLE `t` (
  `id` int(11) NOT NULL,`k` int(11) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id,k) values(1,1) ;
複製程式碼
事務A 事務B
begin
begin
update t set k= k+1 where id=1;
commit;
update t set k = k+1 where id=1;
select k from t where id =1;
commit;

在這個例子中,我們做如下假設:

  1. 事務A、B的版本號分別是100、200,且當前系統裡只有這3個事務;
  2. 三個事務開始前,(1,1)這一行資料的row trx_id是90。

READ COMMITTED —— 每次讀取資料前都生成一個ReadView

繼續上面的例子,假設現在有一個使用READ COMMITTED隔離級別的事務開始執行:

# 使用READ COMMITTED隔離級別的事務
BEGIN;

# SELECT1:Transaction 100、200未提交
select k from t where id=1 ; # 得到值為1
複製程式碼

這個SELECT1的執行過程如下:

  • 在執行SELECT語句時會先生成一個ReadView,ReadView的m_ids列表的內容就是[100,200],min_trx_id為100,max_trx_id為201,creator_trx_id為0。
  • 然後從版本鏈中挑選可見的記錄,最新的版本trx_id值為200,在m_ids列表內,所以不符合可見性要求
  • 下一個版本的trx_id值也為100,也在m_ids列表內,所以也不符合要求,繼續跳到下一個版本。
  • 下一個版本的trx_id值為90,小於ReadView中的min_trx_id值100,所以這個版本是符合要求的。

之後,我們把事務B的事務提交一下,然後再到剛才使用READ COMMITTED隔離級別的事務中繼續查詢,如下:

# 使用READ COMMITTED隔離級別的事務
BEGIN;

# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到值為1

# SELECT2:Transaction 200提交,Transaction 100未提交
SELECT * FROM hero WHERE number = 1; # 得到值為2
複製程式碼

這個SELECT2的執行過程如下:

  • 在執行SELECT語句時會又會單獨生成一個ReadView,該ReadView的m_ids列表的內容就是[100](事務id為200的那個事務已經提交了,所以再次生成快照時就沒有它了),min_trx_id為100,max_trx_id為201,creator_trx_id為0。
  • 然後從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本trx_id值為100,在m_ids列表內,所以不符合可見性要求
  • 下一個版本的trx_id值為200,小於max_trx_id,並且不在m_ids列表中,所以可見,返回的值為2

REPEATABLE READ —— 在第一次讀取資料時生成一個ReadView 假設現在有一個使用REPEATABLE READ隔離級別的事務開始執行:

# 使用REPEATABLE READ隔離級別的事務
BEGIN;

# SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1; # 得到值為1
複製程式碼

這個SELECT1的執行過程如下:

  • 在執行SELECT語句時會先生成一個ReadView,ReadView的m_ids列表的內容就是[100,200],min_trx_id為100,max_trx_id為201,creator_trx_id為0。
  • 然後從版本鏈中挑選可見的記錄,該版本的trx_id值為100,在m_ids列表內,所以不符合可見性要求
  • 下一個版本該版本的trx_id值為200,也在m_ids列表內,所以也不符合要求,繼續跳到下一個版本。
  • 下一個版本的trx_id值為90,小於ReadView中的min_trx_id值100,所以這個版本是符合要求的。

之後,我們把事務B的事務提交一下 然後再到剛才使用REPEATABLE READ隔離級別的事務中繼續查詢:

# 使用REPEATABLE READ隔離級別的事務
BEGIN;

# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到值為1

# SELECT2:Transaction 200提交,Transaction 100未提交
SELECT * FROM hero WHERE number = 1; # 得到值為1
複製程式碼

這個SELECT2的執行過程如下:

  • 因為當前事務的隔離級別為REPEATABLE READ,而之前在執行SELECT1時已經生成過ReadView了,所以此時直接複用之前的ReadView,之前的ReadView的m_ids列表的內容就是[100,200],min_trx_id為100,max_trx_id為201,creator_trx_id為0。
  • 然後從版本鏈中挑選可見的記錄,該版本的trx_id值為100,在m_ids列表內,所以不符合可見性要求
  • 下一個版本該版本的trx_id值為200,也在m_ids列表內,所以也不符合要求,繼續跳到下一個版本。
  • 下一個版本的trx_id值為90,小於ReadView中的min_trx_id值100,所以這個版本是符合要求的。

原文連結 一篇文章帶你掌握mysql的一致性檢視(MVCC)