提到事務,你肯定會想到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,所以這個版本是符合要求的。