1遍愿、什么是MVCC
MVCC(Multi-Version Concurrency Control)即多版本并發(fā)控制。MVCC 是一種并發(fā)控制的方法耘斩,一般在數(shù)據(jù)庫管理系統(tǒng)中沼填,實現(xiàn)對數(shù)據(jù)庫的并發(fā)訪問。MVCC使得大部分支持行鎖的事務引擎括授,不再單純的使用行鎖來進行數(shù)據(jù)庫的并發(fā)控制坞笙,取而代之的是把數(shù)據(jù)庫的行鎖與行的多個版本結(jié)合起來岩饼,只需要很小的開銷,就可以實現(xiàn)非鎖定讀薛夜,從而大大提高數(shù)據(jù)庫系統(tǒng)的并發(fā)性能籍茧。
如果有人從數(shù)據(jù)庫中讀數(shù)據(jù)的同時,有另外的人寫入數(shù)據(jù)梯澜,有可能讀數(shù)據(jù)的人會看到『半寫』或者不一致的數(shù)據(jù)寞冯。有很多種方法來解決這個問題,叫做并發(fā)控制方法晚伙。最簡單的方法吮龄,通過加鎖,讓所有的讀者等待寫者工作完成咆疗,但是這樣效率會很差漓帚。MVCC 使用了一種不同的手段,每個連接到數(shù)據(jù)庫的讀者午磁,在某個瞬間看到的是數(shù)據(jù)庫的一個快照胰默,寫者寫操作造成的變化在寫操作完成之前(或者數(shù)據(jù)庫事務提交之前)對于其他的讀者來說是不可見的。
基于提升并發(fā)性能的考慮漓踢,各大數(shù)據(jù)庫廠商的事務型存儲引擎一般都同時實現(xiàn)了多版本并發(fā)控制(MVCC)牵署。不僅是MySQL,包括Oracle喧半、PostgreSQL等其他數(shù)據(jù)庫系統(tǒng)也都實現(xiàn)了奴迅。MVCC就像是Java語言中的接口,各個數(shù)據(jù)庫廠商的實現(xiàn)機制不盡相同挺据∪【撸可以認為MVCC是行級鎖的一個變種,但是它在很多情況下避免了加鎖操作扁耐,因此開銷更低暇检。雖然實現(xiàn)機制有所不同,但大都實現(xiàn)了非阻塞的讀操作婉称,寫操作也只是鎖定必要的行块仆。MVCC會保存某個時間點上的數(shù)據(jù)快照。這意味著事務可以看到一個一致的數(shù)據(jù)視圖王暗,不管他們需要跑多久悔据。這同時也意味著不同的事務在同一個時間點看到的同一個表的數(shù)據(jù)可能是不同的。前面說到不同的存儲引擎的MVCC實現(xiàn)是不同的俗壹,典型的有樂觀并發(fā)控制和悲觀并發(fā)控制科汗。
MVCC實現(xiàn)的讀寫不阻塞正如其名:多版本并發(fā)控制---->通過一定機制生成一個數(shù)據(jù)請求時間點的一致性數(shù)據(jù)快照(Snapshot),并用這個快照來提供一定級別(語句級或事務級)的一致性讀取绷雏。從用戶的角度來看头滔,好像是數(shù)據(jù)庫可以提供同一數(shù)據(jù)的多個版本怖亭。
2、MySQL的InnoDB存儲引擎實現(xiàn)MVCC的策略
來看看InnoDB的MVCC是怎么樣的吧坤检,以下摘抄自《高性能MySQL》依许。
INSERT:InnoDB為新插入的每一行保存當前系統(tǒng)版本號作為行版本號。
DELETE:InnoDB為刪除的每一行保存當前系統(tǒng)版本號作為行刪除標識缀蹄。
UPDATE:InnoDB為插入一行新記錄,保存當前系統(tǒng)版本號作為行版本號膘婶,同時保存當前系統(tǒng)版本號到原來的行作為刪除標識(這只是理論缺前,innoDB實際是通過undo log來備份舊記錄的)。
在每一行數(shù)據(jù)中額外保存兩個隱藏的列:當前行創(chuàng)建時的版本號和刪除時的版本號(可能為空悬襟,其實還有一列稱為回滾指針衅码,用于事務回滾,不在本文范疇)脊岳。這里的版本號并不是實際的時間值逝段,而是系統(tǒng)版本號。每開始新的事務割捅,系統(tǒng)版本號都會自動遞增奶躯。事務開始時刻的系統(tǒng)版本號會作為事務的版本號,用來和查詢每行記錄的版本號進行比較亿驾。每個事務又有自己的版本號嘹黔,這樣事務內(nèi)執(zhí)行CRUD操作時,就通過版本號的比較來達到數(shù)據(jù)版本控制的目的莫瞬。
innoDB存儲的最基本row中包含一些額外的存儲信息 DATA_TRX_ID儡蔓、DATA_ROLL_PTR、DB_ROW_ID疼邀、DELETE BIT喂江。
DATA_TRX_ID標記了最新更新這條行記錄的transaction id,每處理一個事務旁振,其值自動+1
DATA_ROLL_PTR 指向當前記錄項的rollback segment的undo log記錄获询,找之前版本的數(shù)據(jù)就是通過這個指針
DB_ROW_ID,當由innodb自動產(chǎn)生聚集索引時拐袜,聚集索引包括這個DB_ROW_ID的值筐付,否則聚集索引中不包括這個值,這個用于索引當中
DELETE BIT位用于標識該記錄是否被刪除阻肿,這里的不是真正的刪除數(shù)據(jù)瓦戚,而是標志出來的刪除,真正意義的刪除是在commit的時候丛塌。
1较解、初始插入數(shù)據(jù)行
F1~F6是某行列的名字畜疾,1~6是其對應的數(shù)據(jù)。后面三個隱含字段分別對應該行的事務號和回滾指針印衔,假如這條數(shù)據(jù)是剛INSERT的啡捶,可以認為ID為1,其他兩個字段為空
2奸焙、事務1更改該行的各字段的值
當事務1更改該行的值時瞎暑,會進行如下操作:
用排他鎖鎖定該行
記錄redo log
把該行修改前的值Copy到undo log,即上圖中下面的行
修改當前行的值与帆,填寫事務編號了赌,使回滾指針指向undo log中的修改前的行
3、事務2修改該行的值
與事務1相同玄糟,此時undo log中有兩行記錄勿她,并且通過回滾指針連在一起。因此阵翎,如果undo log一直不刪除逢并,則會通過當前記錄的回滾指針回溯到該行創(chuàng)建時的初始內(nèi)容,所幸的是在Innodb中存在purge線程郭卫,它會查詢那些比現(xiàn)在最老的活動事務還早的undo log砍聊,并刪除它們,從而保證undo log文件不至于無限增長贰军。
當事務正常提交時只需要更改事務狀態(tài)為COMMIT即可辩恼,不需做其他額外的工作,而Rollback則稍微復雜點谓形,需要根據(jù)當前回滾指針從undo log中找出事務修改前的版本并恢復灶伊。如果事務影響的行非常多,回滾則可能會變的效率不高寒跳,根據(jù)經(jīng)驗值沒事務行數(shù)在1000~10000之間聘萨,Innodb效率還是非常高的。很顯然童太,Innodb是一個COMMIT效率比Rollback高的存儲引擎米辐。
下面用更淺顯易懂的例子說明 MVCC 下的 INSERT/DELETE/UPDATE/SELECT 操作。
假如 test 表有兩個字段 name 和 age书释;MVCC 的三個隱藏列字段名為 transaction_id翘贮、 create_version 和 delete_version。
insert
update
delete
滿足以下兩個條件的記錄才能被 select 讀取出來:
delete_version 未定義或者大于 select 所在事務的 delete_version 的行
create_version 小于或等于 select 所在事務的的 create_version的行
通過這個例來看下為什么MVCC 在 REPEATABLE READ 隔離級別下能解決幻讀爆惧。假如有個事務開始于 update 之后 delete 之前狸页,且結(jié)束于 delete 之后,如下:
start transaction; //假如事務 id = 2.5 select * from test; //執(zhí)行時間在 update 之后 delete 之前 select * from test; //執(zhí)行時間在 delete 之后 commit;
如果不使用 MVCC 第一條 select * from test 能讀到 1 條記錄,而 第二條將讀取到 0 條記錄芍耘,同一事務中多次 select 范圍查詢讀取到的記錄不一致即幻讀址遇。而使用 MVVC 之后,兩條select 語句讀取到的記錄相同斋竞。
眾所周知地是更新(update倔约、insert、delete)是一個事務過程坝初,在Innodb中浸剩,查詢也是一個事務,只讀事務鳄袍。當讀寫事務并發(fā)訪問同一行數(shù)據(jù)時绢要,能讀到什么樣的內(nèi)容則依賴事務級別:
READ_UNCOMMITTED,讀未提交畦木,讀事務直接讀取主記錄,無論更新事務是否完成
READ_COMMITTED砸泛,讀已提交十籍,讀事務每次都讀取距離undo log最近的那個版本,因此兩次對同一字段的讀可能讀到不同的數(shù)據(jù)(幻讀)唇礁,但能保證每次都讀到最新的數(shù)據(jù)
REPEATABLE_READ勾栗,每次都讀取指定的版本,這樣保證不會產(chǎn)生幻讀盏筐,但可能讀不到最新的數(shù)據(jù)
SERIALIZABLE围俘,鎖表,讀寫相互阻塞琢融,使用較少
MVCC 只在 REPEATABLE READ 和 READ COMMITTED 兩個隔離級別下工作界牡。其他兩個隔離級別都和MVCC不兼容,因為 READ UNCOMMITTED 總是讀取最新的數(shù)據(jù)行漾抬,而不是符合當前事務版本的數(shù)據(jù)行宿亡。而 SERIALIZABLE則會對所有讀取的行都加鎖。
讀事務一般有SELECT語句觸發(fā)纳令,在Innodb中保證其非阻塞挽荠,但帶FOR UPDATE的SELECT除外,帶FOR UPDATE的SELECT會對行加排他鎖平绩,等待更新事務完成后讀取其最新內(nèi)容圈匆。就整個Innodb的設計目標來說,就是提供高效的捏雌、非阻塞的查詢操作跃赚。
3、InnoDB實現(xiàn)的MVCC有何特殊性
上述更新前建立undo log性湿,根據(jù)各種策略讀取時非阻塞就是MVCC来累,undo log中的行就是MVCC中的多版本砚作,這個可能與我們所理解的MVCC有較大的出入,一般我們認為MVCC有下面幾個特點:
每行數(shù)據(jù)都存在一個版本嘹锁,每次數(shù)據(jù)更新時都更新該版本
修改時Copy出當前版本隨意修改葫录,各個事務之間無干擾
保存時比較版本號,如果成功則commit并覆蓋原記錄领猾;失敗則放棄copy(rollback)
就是每行都有版本號米同,保存時根據(jù)版本號決定是否成功,聽起來含有樂觀鎖的味道摔竿,而Innodb的實現(xiàn)方式是:
事務以排他鎖的形式修改原始數(shù)據(jù)
把修改前的數(shù)據(jù)存放于undo log面粮,通過回滾指針與主數(shù)據(jù)關聯(lián)
修改成功(commit)啥都不做,失敗則恢復undo log中的數(shù)據(jù)(rollback)
二者最本質(zhì)的區(qū)別是继低,當修改數(shù)據(jù)時是否要排他鎖定熬苍,如果鎖定了還算不算是MVCC。
MVCC可以保證不阻塞地讀到一致的數(shù)據(jù)袁翁。但是MVCC理論并沒有對實現(xiàn)細節(jié)做約束柴底,為此不同的數(shù)據(jù)庫的語義有所不同,比如:
postgres 對寫操作也是樂觀并發(fā)控制粱胜;在表中保存同一行數(shù)據(jù)記錄的多個不同版本柄驻,每次寫操作都是創(chuàng)建,而回避更新焙压; 在事務提交時鸿脓,按版本號檢查當前事務提交的數(shù)據(jù)是否存在寫沖突,則拋異常告知用戶涯曲,回滾事務野哭;
innodb 則只對讀無鎖,寫操作仍是上鎖的悲觀并發(fā)控制幻件,這也意味著虐拓,innodb中只能見到因死鎖和不變性約束而回滾,而見不到因為寫沖突而回滾傲武; 不像 postgres 那樣對數(shù)據(jù)修改在表中創(chuàng)建新紀錄蓉驹,而是每行數(shù)據(jù)只在表中保留一份,在更新數(shù)據(jù)時上行鎖揪利,同時將舊版數(shù)據(jù)寫入 undo log态兴; 表和 undo log 中行數(shù)據(jù)都記錄著事務ID,在檢索時根據(jù)事務隔離級別去讀取行數(shù)據(jù)疟位≌叭螅可見 MVCC中的寫操作仍可以按悲觀并發(fā)控制實現(xiàn);
MVCC解決的問題是讀寫互相不阻塞的問題,每次更新都產(chǎn)生一個新的版本绍撞,讀的話可以讀歷史版本正勒。試想,如果一個數(shù)據(jù)只有一個版本傻铣,那么多個事務對這個數(shù)據(jù)進行讀寫是不是需要讀寫鎖來保護?
一個讀寫事務在運行的過程中在訪問數(shù)據(jù)之前先加讀/寫鎖這種實現(xiàn)叫做悲觀鎖章贞,悲觀體現(xiàn)在先加鎖,獨占數(shù)據(jù)非洲,防止別人加鎖鸭限。
樂觀鎖呢,讀寫事務两踏,在真正的提交之前败京,不加讀/寫鎖,而是先看一下數(shù)據(jù)的版本/時間戳梦染,等到真正提交的時候再看一下版本/時間戳赡麦,如果兩次相同,說明別人期間沒有對數(shù)據(jù)進行過修改帕识,那么就可以放心提交泛粹。
樂觀體現(xiàn)在,訪問數(shù)據(jù)時不提前加鎖渡冻。在資源沖突不激烈的場合戚扳,用樂觀鎖性能較好忧便。
如果資源沖突嚴重族吻,樂觀鎖的實現(xiàn)會導致事務提交的時候經(jīng)常看到別人在他之前已經(jīng)修改了數(shù)據(jù)珠增,然后要進行回滾或者重試超歌,還不如一上來就加鎖。
4蒂教、快照讀與當前讀
快照讀就是讀取數(shù)據(jù)的時候會根據(jù)一定規(guī)則讀取事務可見版本的數(shù)據(jù)(可能是過期的數(shù)據(jù))巍举,不用加鎖。
當前讀, 讀取的是最新版本, 并且對讀取的記錄加鎖凝垛,保證其他事務不會再并發(fā)的修改這條記錄懊悯,避免出現(xiàn)安全問題。 使用當前讀的場景:
select…lock in share mode (共享讀鎖)
select…for update
update
delete
insert
使用快照讀的場景:
- 單純的select操作梦皮,不包括上述 select … lock in share mode炭分、select … for update
通過舉例來理解快照讀與當前讀吧:MySQL innoDB的RR隔離級別下,假設你開啟了兩個事務剑肯,分別是A和B捧毛,這里有個張user表,里面有四條數(shù)據(jù)。
CREATE TABLE
user
(
id
int(11) NOT NULL,
name
varchar(64) NOT NULL,
PRIMARY KEY (id
),
KEYname
(name
) )
ENGINE=InnoDB;
insert into user values(0,"Jack"),(5,"Tom"), (10,"Jerry"),(15,"ZhangSan");
當你執(zhí)行select *之后呀忧,在A與B事務中都會返回4條一樣的數(shù)據(jù)师痕,這是不用想的,RR隔離級別下當執(zhí)行普通的select查詢時而账,innodb默認會執(zhí)行快照讀胰坟,相當于就是給你目前的狀態(tài)找了一張照片,以后執(zhí)行select 的時候就會返回當前照片里面的數(shù)據(jù)福扬,當其他事務提交了也對你不造成影響腕铸,和你沒關系,這就實現(xiàn)了可重復讀铛碑,那這個照片是什么時候生成的呢狠裹?
不是開啟事務的時候,是當你第一次執(zhí)行select的時候汽烦,也就是說涛菠,當A開啟了事務,然后沒有執(zhí)行任何操作撇吞,這時候B insert了一條數(shù)據(jù)然后commit俗冻,這時候A在事務中執(zhí)行select,那么就能看到有B在自己在事務中添加的那條數(shù)據(jù)…牍颈,在這之后無論再有其他事務commit都沒有關系迄薄,因為照片已經(jīng)生成了,而且不會再生成了煮岁,以后都會參考這張照片讥蔽。
總結(jié)
所謂的MVCC(Multi-Version Concurrency Control 多版本并發(fā)控制)指的就是在使用讀已提交(READ COMMITTD)、可重復讀(REPEATABLE READ)這兩種隔離級別的事務在執(zhí)行普通的SELECT操作時訪問記錄的版本鏈的過程画机,這樣子可以使不同事務的讀-寫冶伞、寫-讀操作并發(fā)執(zhí)行,從而提升系統(tǒng)性能步氏。
這兩個隔離級別的一個很大不同就是:生成ReadView的時機不同响禽,READ COMMITTD在每一次進行普通SELECT操作前都會生成一個ReadView,而REPEATABLE READ只在第一次進行普通SELECT操作前生成一個ReadView荚醒,數(shù)據(jù)的可重復讀其實就是ReadView的重復使用芋类。
InnoDB通過為每一行記錄添加兩個額外的隱藏的值來實現(xiàn)MVCC,這兩個值一個記錄這行 數(shù)據(jù)何時被創(chuàng)建界阁,另外一個記錄這行數(shù)據(jù)何時過期(或者被刪除)侯繁。但是InnoDB并不存儲這些事件發(fā)生時的實際時間,相反它只存儲這些事件發(fā)生時的系統(tǒng)版本號铺董。這是一個隨著事務的創(chuàng)建而不斷增長的數(shù)字巫击。每個事務在事務開始時會記錄它自己的系統(tǒng)版本號禀晓。每個查詢必須去檢查每行數(shù)據(jù)的版本號與事務的版本號是否相同。
這種額外的記錄所帶來的結(jié)果就是對于大多數(shù)查詢來說根本就不需要獲得一個鎖坝锰。
他們只是簡單地以最快的速度來讀取數(shù)據(jù)粹懒,確保只選擇符合條件的行。這個方案的缺點在于存儲引擎必須為每一行存儲更多的數(shù)據(jù)顷级,做更多的檢查工作凫乖,處理更多的善后操作。
使用MVCC多版本并發(fā)控制比鎖定模型的主要優(yōu)點是在MVCC里弓颈, 對檢索(讀)數(shù)據(jù)的鎖要求與寫數(shù)據(jù)的鎖要求不沖突帽芽, 所以讀不會阻塞寫,而寫也從不阻塞讀翔冀。
在數(shù)據(jù)庫里也有表和行級別的鎖定機制导街, 用于給那些無法輕松接受 MVCC 行為的應用。 不過纤子,恰當?shù)厥褂?MVCC 總會提供比鎖更好地性能搬瑰。