多版本並發控制技術已經被廣泛運用於各大資料庫系統中,如Oracle,MS SQL Server 2005+, Postgresql, Firebird, Maria等等,開源資料庫MYSQL中流行的INNODB引擎也採用了類似的並發控制技術.本文就將結合實例來解析不同事務隔離等級下INNODB的MVCC實現原理.
1 MVCC概述
1.1 MVCC簡介
MVCC (Multiversion Concurrency Control),即多版本併發控制技術,它使得大部分支援行鎖的事務引擎,不再單純的使用行鎖來進行資料庫的併發控制,取而代之的是,把資料庫的行鎖與行的多個版本結合起來,只需要很小的開銷,就可以實現非鎖定讀,從而大大提高資料庫系統的併發效能.
1.2 實現原理
MVCC可以提供基於某個時間點的快照,使得對於事務看來,總是可以提供與事務開始時刻相一致的資料,而不管這個事務執行的時間有多長.所以在不同的事務看來,同一時刻看到的相同行的資料可能是不一樣的,即一個行可能有多個版本.是否聽起來不可思議呢?
原來,為了實現mvcc, innodb對每一行都加上了兩個隱含的列,其中一列儲存行被更新的”時間”,另外一列儲存行被刪除的”時間”. 但是innodb儲存的並不是絕對的時間,而是與時間對應的資料庫系統的版本號,每當一個事務開始的時候,innodb都會給這個事務分配一個遞增的版本號,所以版本號也可以被認為是事務號.對於每一個”查詢”語句,innodb都會把這個查詢語句的版本號同這個查詢語句遇到的行的版本號進行對比,然後結合不同的事務隔離等級,來決定是否返回該行.
下面分別以select、delete、 insert、 update語句來說明:
1) SELECT
對於select語句,只有同時滿足了下面兩個條件的行,才能被返回:
•行的被修改版本號小於或者等於該事務號
•行的被刪除版本號要麼沒有被定義,要麼大於事務的版本號:行的刪除版本號如果沒有被定義,說明該行沒有被刪除過;如果刪除版本號大於當前事務的事務號,說明該行是被該事務後面啟動的事務刪除的,由於是repeatable read隔離等級,後開始的事務對資料的影響不應該被先開始的事務看見,所以該行應該被返回.
2) INSERT
對新插入的行,行的更新版本被修改為該事務的事務號
3) DELETE
對於刪除,innodb直接把該行的被刪除版本號設定為當前的事務號,相當於標記為刪除,而不是實際刪除
4) UPDATE
在更新行的時候,innodb會把原來的行復制一份到回滾段中,並把當前的事務號作為該行的更新版本
1.3 MVCC的優缺點
上述策略的結果就是,在讀取資料的時候,innodb幾乎不用獲得任何鎖, 每個查詢都通過版本檢查,只獲得自己需要的資料版本,從而大大提高了系統的併發度.
這種策略的缺點是,為了實現多版本,innodb必須對每行增加相應的欄位來儲存版本資訊,同時需要維護每一行的版本資訊,而且在檢索行的時候,需要進行版本的比較,因而降低了查詢的效率;innodb還必須定期清理不再需要的行版本,及時回收空間,這也增加了一些開銷
2 INNODB支援的事務隔離等級
INNODB支援並實現了ISO標準的4個事務隔離等級,即 READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE.
1) READ UNCOMMITTED (可以讀未提交的): 查詢可以讀取到其他事務正在修改的資料,即使其他事務的修改還沒有提交.這種隔離等級無法避免髒讀.
2) READ COMMITTED(只可以讀已經提交的):其他事務對資料庫的修改,只要已經提交,其修改的結果就是可見的,與這兩個事務開始的先後順序無關.這種隔離等級避免了髒讀,但是無法實現可重複讀,甚至有可能產生幻讀.
3) REPEATABLE READ(可重複讀):比read committed更進了一步,它只能讀取在它開始之前已經提交的事務對資料庫的修改,在它開始以後,所有其他事務對資料庫的修改對它來說均不可見.從而實現了可重複讀,但是仍有可能幻讀
4) SERIALIZABLE(可序列化):這是事務隔離等級的最高階別.其實現原理就是對於所有的query,即使是查詢,也會加上讀鎖,避免其他事務對資料的修改.所以它成功的避免了幻讀.但是代價是,資料庫系統的併發處理能力大大降低,所以它不會被用到生產系統中.
我們對MVCC和標準事務隔離等級有所瞭解以後,再結合例項來看看其具體表現吧.
3 不同事務隔離等級下的MVCC實現
MVCC由於其實現原理,只支援read committed和repeatable read隔離等級,下面分別舉例詳細說明:
每次開始之前,都先執行如下的語句:
create database if not exists mydb;
use mydb;
drop table if exists emp;
create table `emp` ( `empno` int(11) not null auto_increment, `ename` varchar(20) default null, Primary key (empno)) engine=innodb default charset=gbk;
insert into emp values(100, "yuxiangang") ;
insert into emp values(200,"2zhaoyinggang");
insert into emp values(300,"3yihongbin");
3.1 read committed隔離等級
說明:session 1和session 2表示訪問同一個資料庫的兩個不同的會話.行號用來代表不同的語句執行的時間點.
行號 |
session 1 |
session 2 |
1 |
set transaction isolation level read committed; |
|
2 |
start transaction; |
|
3 |
select * from emp where empno>=100; 查詢的結果為: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1yuxiangang | | 200 | 2zhaoyinggang | | 300 | 3yihongbin | +-------+-------+ |
|
4 |
|
set transaction isolation level read committed; |
5 |
|
start transaction; |
6 |
|
update emp set ename=1 where empno=100; delete from emp where empno=200; |
說明: 修改一行,然後刪除一行,但是事務不提交. |
||
7 |
select * from emp where empno>=100; 查詢的結果為: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1yuxiangang | | 200 | 2zhaoyinggang | | 300 | 3yihongbin | +-------+-------+ |
|
說明:會話2的事務沒有提交,所以會話1看不到會話2的事務對資料庫資料的修改.但是實際上修改已經發生,會話1獲取的被修改或者刪除的資料,都來自於回滾段.這是通過MVCC來實現的. |
||
8 |
|
commit; |
說明: 會話2提交 |
||
9 |
select * from emp where empno>=100; 查詢的結果為: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 300 | 3yihongbin | +-------+-------+ |
|
說明:當事務2提交以後,由於會話1採用的是read committed隔離等級,所以會話2的提交馬上會被會話1的事務看見.對於會話1來說,第一次執行select * from emp where empno>=100;與第二次執行該語句,兩次看到的結果不一樣,第一次讀看到了3行,第二次只看到了2行,就像發生了幻覺,稱之為幻讀;第一次看到100對應的ename為1yuxiangang,第二次看到的100對應的是1,兩次獲取的資料內容不一樣,稱之為不可重複讀. |
3.2 repeatable read隔離等級
注意:先執行開頭的所有sql語句.
行號 |
session 1 |
session 2 |
session 3 |
session 4 |
1 |
set transaction isolation level repeatable read; |
|
|
|
2 |
start transaction; |
|
|
|
3 |
select * from emp where empno>=100; 查詢的結果為: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1yuxiangang | | 200 | 2zhaoyinggang | | 300 | 3yihongbin | +-------+-------+ |
|
|
|
4 |
|
|
|
set @@session.autocommit=1; |
說明: 這裡讓會話4可以自動提交,便於觀察它對前面3個會話的影響 |
||||
|
|
|
|
update emp set ename=1 where empno=100; insert into emp values(400,"4chj"); |
說明: 會話4先更新一行資料,然後插入一行資料,並自動提交 |
||||
5 |
select * from emp where empno>=100; 查詢的結果為: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1yuxiangang | | 200 | 2zhaoyinggang | | 300 | 3yihongbin | +-------+-------+ |
|
|
|
說明: 會話1執行查詢,兩次查詢得到的結果一樣.它看不到會話4對資料庫的修改,雖然會話4的事務已經提交.這是因為會話4的事務是在會話1的事務之後才開始.從這裡也可以看出,repeatable read實現了可重複讀 |
||||
6 |
|
set transaction isolation level repeatable read;start transaction; |
|
|
7 |
|
select * from emp where empno>=100; 查詢的結果為: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2zhaoyinggang | | 300 | 3yihongbin | | 400 | 4chj | +-------+-------+ |
|
|
說明: 會話2是在會話4之後開始的,所以它看到了會話4對資料庫的修改.同時可以看到,相同的查詢語句,不同的事務來執行的時候,得到的結果不一樣.會話2與會話3執行相同的查詢就得到不一樣的結果. |
||||
8 |
|
|
|
update emp set ename=2 where empno=200; |
9 |
|
|
set transaction isolation level repeatable read;start transaction; |
|
10 |
|
|
select * from emp where empno>=100;查詢的結果為: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2 | | 300 | 3yihongbin | | 400 | 4chj | +-------+-------+ |
|
說明: 同樣,這個會話查詢到的結果與會話1和會話2的結果也不一樣.而且會話3看到了會話4對資料庫的修改. |
||||
11 |
|
|
|
update emp set ename=4 where empno=400; |
12 |
|
|
|
select * from emp where empno>=100; 查詢的結果為: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2 | | 300 | 3yihongbin | | 400 | 4 | +-------+-------+ |
說明: 事務總是可以看到自身對資料庫資料的修改,儘管別的事務可能看不到這種修改 |
||||
13 |
select * from emp where empno>=100; 查詢的結果為: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1yuxiangang | | 200 | 2zhaoyinggang | | 300 | 3yihongbin | +-------+-------+ |
select * from emp where empno>=100; 查詢的結果為: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2zhaoyinggang | | 300 | 3yihongbin | | 400 | 4chj | +-------+-------+ |
select * from emp where empno>=100; 查詢的結果為: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2 | | 300 | 3yihongbin | | 400 | 4chj | +-------+-------+ |
select * from emp where empno>=100; 查詢的結果為: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2 | | 300 | 3yihongbin | | 400 | 4 | +-------+-------+ |
說明: 從上面的結果可以很清晰的看到:會話1,2,3,4執行相同的語句,即使是在同一時刻,他們看到的資料都可能不一樣:對於empno為100的行,有 100 1yuxiangang 和 100 1兩個版本;對於empno為200的行,有 200 2zhaoyinggang 和200 2兩個版本…,而每一行資料都可能存在多個版本,那麼這些行組合起來得到的結果集的版本就更是不計其數,這就是資料庫多版本的由來.MVCC就是通過事務發生的不同的時間點,與資料行的版本來進行對比,從而取回與事務開始的時間點相一致的資料,來實現非阻塞的一致讀. |
||||
14 |
commit; |
commit; |
commit; |
commit; |
15 |
select * from emp where empno>=100; 查詢的結果為: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2 | | 300 | 3yihongbin | | 400 | 4 | +-------+-------+ |
select * from emp where empno>=100; 查詢的結果為: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2 | | 300 | 3yihongbin | | 400 | 4 | +-------+-------+ |
select * from emp where empno>=100; 查詢的結果為: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2 | | 300 | 3yihongbin | | 400 | 4 | +-------+-------+ |
select * from emp where empno>=100; 查詢的結果為: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2 | | 300 | 3yihongbin | | 400 | 4 | +-------+-------+ |
說明: 當所有事務都提交後,他們看到的結果都是一樣的 |
4 相關的Q&A
4.1 為什麼select count(*)在myisam表上很快,而在Innodb的表上很慢?
因為innodb採用了MVCC技術,對於相同的行,可能同時存在多個版本,innodb必須根據查詢的時間來過濾掉一些行,才能得出結果,必然要執行全表掃描,而全表掃描是非常耗時的.對於myisam的表,任何行都只有一個版本,mysql甚至不需要掃描就可以直接返回精確的統計結果,我們用explain也可以看到,對於myisam的表,執行select count(*)的時候,mysql顯示” Select tables optimized away”,查詢直接被優化了;而對於innodb的表,可能是全表掃描,也可能是”using index”,總之,速度肯定會比myisam的錶慢很多.
4.2 我的資料庫只是頻繁更新,沒有插入新資料,但是為什麼表空間佔用會越來越大?
如果你在資料庫中執行了大事務, innodb就會把被修改資料的前映像存放到稱為回滾段的公共表空間中,而且對於索引和表中的行的多個版本,如果innodb來不及purge,或者這些行因為要提供一致讀而不能被purge,就會佔用越來越多的空間,甚至有可能短時間撐爆你的硬碟.所以應用程式中需要合理控制事務的大小.
4.3 能禁用MVCC嗎?
禁用MVCC可以降低innodb引擎的開銷,而同時innodb又可以支援外來鍵約束,可以實現自動恢復.MVCC本身不支援read uncommitted等級,所以可以通過設定transaction_isolation = read uncommitted 來禁用MVCC.但是任何改變innodb預設隔離等級的操作,都會起到innodb_locks_unsafe_for_binlog=off類似的效果,這會導致諸如insert into t select * from t_src 之類的語句不再給源表t_src加鎖,也不再使用innodb的間隙鎖,從而產生幻讀,直接導致binlog中記錄的sql語句不能正確的序列化,從而主從資料庫的資料不再一致,而且基於binlog的增量備份也不再有效.所以除非不需要記錄binlog,否則別這麼做.當然我們可以這樣做來優化從庫的效能,因為從庫不需要記錄binlog.
4.4 何時使用char型別,何時使用varchar型別的列?
在使用myisam引擎的情況下,定長表雖然可能佔用較多的儲存空間,但是它會加快檢索和全表掃描的速度,此時適合選用char的列,而對於表中的變長的列,可以採用分表的方法把變長的列拆分出去,提高定長表的檢索效能.而如果使用的是innodb的引擎,由於innodb的mvcc策略的實施,char資料型別相對於varchar型別幾乎沒有任何優勢,反而varchar列可能節省更多的儲存空間,建議使用varchar資料型別.
轉:http://blog.csdn.net/aoxida/article/details/50689619