一篇文章帶你掌握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; |
在這個例子中,我們做如下假設:
- 事務A、B的版本號分別是100、200,且當前系統裡只有這3個事務;
- 三個事務開始前,(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,所以這個版本是符合要求的。