MVCC(Multi-Version Concurrent Control,多版本併發控制)簡介 php
MVCC(Multi-Version Concurrent Control),即多版本併發控制協議,普遍使用於數據庫系統。java
在介紹MVCC概念以前,咱們先來想一下數據庫系統裏的一個問題:假設有多個用戶同時讀寫數據庫裏的一行記錄,那麼怎麼保證數據的一致性呢?一個基本的解決方法是對這一行記錄加上一把鎖,將不一樣用戶對同一行記錄的讀寫操做徹底串行化執行,因爲同一時刻只有一個用戶在操做,所以一致性不存在問題。可是,它存在明顯的性能問題:讀會阻塞寫,寫也會阻塞讀,整個數據庫系統的併發性能將大打折扣。mysql
MVCC(Multi-Version Concurrent Control),即多版本併發控制協議,它的目標是在保證數據一致性的前提下,提供一種高併發的訪問性能。在MVCC協議中,每一個用戶在鏈接數據庫時看到的是一個具備一致性狀態的鏡像,每一個事務在提交到數據庫以前對其餘用戶均是不可見的。當事務須要更新數據時,不會直接覆蓋之前的數據,而是生成一個新的版本的數據,所以一條數據會有多個版本存儲,可是同一時刻只有最新的版本號是有效的。所以,讀的時候就能夠保證老是以當前時刻的版本的數據能夠被讀到,不論這條數據後來是否被修改或刪除。sql
在併發讀寫數據庫時,讀操做可能會不一致的數據(髒讀)。爲了不這種狀況,須要實現數據庫的併發訪問控制,最簡單的方式就是加鎖訪問。因爲,加鎖會將讀寫操做串行化,因此不會出現不一致的狀態。可是,讀操做會被寫操做阻塞,大幅下降讀性能。在java concurrent包中,有copyonwrite系列的類,專門用於優化讀遠大於寫的狀況。而其優化的手段就是,在進行寫操做時,將數據copy一份,不會影響原有數據,而後進行修改,修改完成後原子替換掉舊的數據,而讀操做只會讀取原有數據。經過這種方式實現寫操做不會阻塞讀操做,從而優化讀效率。而寫操做之間是要互斥的,而且每次寫操做都會有一次copy,因此只適合讀大於寫的狀況。數據庫
MVCC的原理與copyonwrite相似,全稱是Multi-Version Concurrent Control,即多版本併發控制。在MVCC協議下,每一個讀操做會看到一個一致性的snapshot,而且能夠實現非阻塞的讀。MVCC容許數據具備多個版本,這個版本能夠是時間戳或者是全局遞增的事務ID,在同一個時間點,不一樣的事務看到的數據是不一樣的。session
實現原理: 併發
------------------------------------------------------------------------------------------> 時間軸分佈式
|-------R(T1)-----|ide
|-----------U(T2)-----------|高併發
如上圖,假設有兩個併發操做R(T1)和U(T2),T1和T2是事務ID,T1小於T2,系統中包含數據a = 1(T1),R和W的操做以下:
R:read a (T1)
U:a = 2 (T2)
R(讀操做)的版本T1表示要讀取數據的版本,而以後寫操做纔會更新版本,讀操做不會。在時間軸上,R晚於U,而因爲U在R開始以後提交,因此對於R是不可見的。因此,R只會讀取T1版本的數據,即a = 1。
因爲在update操做提交以前,不能影響已有數據的一致性,因此不會改變舊的數據,update操做會被拆分紅insert + delete。須要標記刪除舊的數據,insert新的數據。只有update提交以後,纔會影響後續的讀操做。而對於讀操做並且,只能讀到在其以前的全部的寫操做,正在執行中的寫操做對其是不可見的。
上面說了一堆的虛的理論,下面來點幹活,看一下MySQL的innodb引擎是如何實現MVCC的。innodb會爲每一行添加兩個字段,分別表示該行建立的版本和刪除的版本,填入的是事務的版本號,這個版本號隨着事務的建立不斷遞增。在repeated read的隔離級別(事務的隔離級別請看這篇文章)下,具體各類數據庫操做的實現:
select:知足如下兩個條件innodb會返回該行數據:(1)該行的建立版本號小於等於當前版本號,用於保證在select操做以前全部的操做已經執行落地。(2)該行的刪除版本號大於當前版本或者爲空。刪除版本號大於當前版本意味着有一個併發事務將該行刪除了。
insert:將新插入的行的建立版本號設置爲當前系統的版本號。
delete:將要刪除的行的刪除版本號設置爲當前系統的版本號。
update:不執行原地update,而是轉換成insert + delete。將舊行的刪除版本號設置爲當前版本號,並將新行insert同時設置建立版本號爲當前版本號。
其中,寫操做(insert、delete和update)執行時,須要將系統版本號遞增。
因爲舊數據並不真正的刪除,因此必須對這些數據進行清理,innodb會開啓一個後臺線程執行清理工做,具體的規則是將刪除版本號小於當前系統版本的行刪除,這個過程叫作purge。
經過MVCC很好的實現了事務的隔離性,能夠達到repeated read級別,要實現serializable還必須加鎖。
Mysql中的MVCC
MySQL究竟是怎麼實現MVCC的?這個問題無數人都在問,但google中並沒有答案,本文嘗試從Mysql源碼中尋找答案。
在Mysql中MVCC是在Innodb存儲引擎中獲得支持的,Innodb爲每行記錄都實現了三個隱藏字段:
6字節的事物ID用來標識該行所述的事務,7字節的回滾指針須要瞭解下Innodb的事務模型。
爲了支持事務,Innbodb引入了下面幾個概念:
下面演示下事務對某行記錄的更新過程:
F1~F6是某行列的名字,1~6是其對應的數據。後面三個隱含字段分別對應該行的事務號和回滾指針,假如這條數據是剛INSERT的,能夠認爲ID爲1,其餘兩個字段爲空。
當事務1更改該行的值時,會進行以下操做:
與事務1相同,此時undo log,中有有兩行記錄,而且經過回滾指針連在一塊兒。
所以,若是undo log一直不刪除,則會經過當前記錄的回滾指針回溯到該行建立時的初始內容,所幸的時在Innodb中存在purge線程,它會查詢那些比如今最老的活動事務還早的undo log,並刪除它們,從而保證undo log文件不至於無限增加。
當事務正常提交時Innbod只須要更改事務狀態爲COMMIT便可,不需作其餘額外的工做,而Rollback則稍微複雜點,須要根據當前回滾指針從undo log中找出事務修改前的版本,並恢復。若是事務影響的行很是多,回滾則可能會變的效率不高,根據經驗值沒事務行數在1000~10000之間,Innodb效率仍是很是高的。很顯然,Innodb是一個COMMIT效率比Rollback高的存儲引擎。聽說,Postgress的實現剛好與此相反。
上述過程確切地說是描述了UPDATE的事務過程,其實undo log分insert和update undo log,由於insert時,原始的數據並不存在,因此回滾時把insert undo log丟棄便可,而update undo log則必須遵照上述過程。
衆所周知地是更新(update、insert、delete)是一個事務過程,在Innodb中,查詢也是一個事務,只讀事務。當讀寫事務併發訪問同一行數據時,能讀到什麼樣的內容則依賴事務級別:
讀事務通常有SELECT語句觸發,在Innodb中保證其非阻塞,但帶FOR UPDATE的SELECT除外,帶FOR UPDATE的SELECT會對行加排他鎖,等待更新事務完成後讀取其最新內容。就整個Innodb的設計目標來講,就是提供高效的、非阻塞的查詢操做。
上述更新前創建undo log,根據各類策略讀取時非阻塞就是MVCC,undo log中的行就是MVCC中的多版本,這個可能與咱們所理解的MVCC有較大的出入,通常咱們認爲MVCC有下面幾個特色:
就是每行都有版本號,保存時根據版本號決定是否成功,聽起來含有樂觀鎖的味道。。。,而Innodb的實現方式是:
兩者最本質的區別是,當修改數據時是否要排他鎖定,若是鎖定了還算不算是MVCC?
Innodb的實現真算不上MVCC,由於並無實現核心的多版本共存,undo log中的內容只是串行化的結果,記錄了多個事務的過程,不屬於多版本共存。但理想的MVCC是難以實現的,當事務僅修改一行記錄使用理想的MVCC模式是沒有問題的,能夠經過比較版本號進行回滾;但當事務影響到多行數據時,理想的MVCC據無能爲力了。
好比,若是Transaciton1執行理想的MVCC,修改Row1成功,而修改Row2失敗,此時須要回滾Row1,但由於Row1沒有被鎖定,其數據可能又被Transaction2所修改,若是此時回滾Row1的內容,則會破壞Transaction2的修改結果,致使Transaction2違反ACID。
理想MVCC難以實現的根本緣由在於企圖經過樂觀鎖代替二段提交。修改兩行數據,但爲了保證其一致性,與修改兩個分佈式系統中的數據並沒有區別,而二提交是目前這種場景保證一致性的惟一手段。二段提交的本質是鎖定,樂觀鎖的本質是消除鎖定,兩者矛盾,故理想的MVCC難以真正在實際中被應用,Innodb只是借了MVCC這個名字,提供了讀的非阻塞而已。
也不是說MVCC就無處可用,對一些一致性要求不高的場景和對單一數據的操做的場景仍是能夠發揮做用的,好比多個事務同時更改用戶在線數,若是某個事務更新失敗則從新計算後重試,直至成功。這樣使用MVCC會極大地提升併發數,並消除線程鎖。
2017-09-25 葉師傅新班已發車 老葉茶館
InnoDB MVCC是事務一啓動就建立read view,仍是何時?
說到事務,咱們不得不先說下什麼是ACID、MVCC、consistent read、read view 等幾個基本概念。
ACID是事務的原子性、一致性、隔離性、持久性4個單詞的首字母縮寫。全部的事務型數據庫系統都遵循這4個特性,InnoDB亦是如此。關於ACID的具體解釋請自行 google/bing。
是multiversion concurrency control的簡稱,也就是多版本併發控制,是個很基本的概念。MVCC的做用是讓事務在並行發生時,在必定隔離級別前提下,能夠保證在某個事務中能實現一致性讀,也就是該事務啓動時根據某個條件讀取到的數據,直到事務結束時,再次執行相同條件,仍是讀到同一份數據,不會發生變化(不會看到被其餘並行事務修改的數據)。
有了 MVCC 就能夠提升事務的並行度,由於能夠利用鎖機制實現資源控制而無需等待其餘事務先執行。
InnoDB MVCC使用的內部快照的意思。在不一樣的隔離級別下,事務啓動時(有些狀況下,多是SQL語句開始時)看到的數據快照版本可能也不一樣。在RR、RC、RU(READ UNCOMMITTED)等幾個隔離級別下會用到 read view。
讀請求基於某個時間點獲得一份那時的數據快照,而無論同時其餘事務對數據的修改。查詢過程當中,若其餘事務修改了數據,那麼就須要從 undo log中獲取舊版本的數據。這麼作能夠有效避免由於須要加鎖(來阻止其餘事務同時對這些數據的修改)而致使事務並行度降低的問題。
在可重複讀(REPEATABLE READ,簡稱RR)隔離級別下,數據快照版本是在第一個讀請求發起時建立的。在讀已提交(READ COMMITTED,簡稱RC)隔離級別下,則是在每次讀請求時都會從新建立一份快照。
一致性讀是InnoDB在RR和RC下處理SELECT請求的默認模式。因爲一致性讀不會在它請求的表上加鎖,其餘事務能夠同時修改數據不受影響。
其實,咱們從上面的解釋已經明白了,在RC隔離級別下,是每一個SELECT都會獲取最新的read view;而在RR隔離級別下,則是當事務中的第一個SELECT請求才建立read view。
咱們經過幾個例子來增強下。
首先,確認隔離級別
yejr@imysql.com [test]>select @@tx_isolation; +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+
測試1:事務啓動後當即發起SELECT請求
session1 | session2 |
---|---|
begin; | begin; |
select * from t1 where a=10; +----+------+---+ | a | b | c | +----+------+---+ | 10 | 8 | 1 | |
select * from t1 where a=10; +----+------+---+ | 10 | 8 | 1 | 事務中第一個SELECT當即建立read view |
update t1 set c=10 where a=10; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
select * from t1 where a=10; +----+------+---+ | 10 | 8 | 1 | 再次讀取,結果仍是同樣 |
commit; 提交事務 |
select * from t1 where a=10; +----+------+---+ | 10 | 8 | 1 | 再次讀取,結果仍然同樣 |
結論可見:RR中第一個SELECT已經建立好read view,以後不會再發生變化
測試2:另外一個事物提交後才發起SELECT請求
session1 | session2 |
---|---|
begin; | begin; |
select * from t1 where a=10; +----+------+---+ | a | b | c | +----+------+---+ | 10 | 8 | 1 | |
|
update t1 set c=10 where a=10; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
|
commit; 提交事務 |
select * from t1 where a=10; +----+------+---+ | 10 | 8 | 10 | session1提交後才發起SELECT,能夠讀取到最新版本 |
結論可見:RR中是發起SELECT時才建立read view,而不是事務剛啓動時就建立
根據上面提到的說法,RC隔離級別下,是每次發起SELECT都會建立read view,也就是每次SELECT都能讀取到已經COMMIT的數據,因此才存在不可重複讀、幻讀 現象。
修改&確認隔離級別
yejr@imysql.com [test]>set session transaction isolation level read committed; yejr@imysql.com [test]>select @@tx_isolation; +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+
開始測試
session1 | session2 |
---|---|
begin; | begin; |
select * from t1 where a=10; +----+------+---+ | a | b | c | +----+------+---+ | 10 | 8 | 101 | |
select * from t1 where a=10; +----+------+---+ | a | b | c | +----+------+---+ | 10 | 8 | 101 | |
update t1 set c=102 where a=10;commit; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 Query OK, 0 rows affected (0.02 sec) |
|
select * from t1 where a=10; +----+------+---+ | 10 | 8 | 102 | session1提交後再次發起SELECT,能夠讀取到最新版本 |
|
begin;update t1 set c=103 where a=10;commit; Query OK, 0 rows affected (0.00 sec) Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 Query OK, 0 rows affected (0.02 sec) |
|
select * from t1 where a=10; +----+------+---+ | 10 | 8 | 103 | 再次發起SELECT,又能夠讀取到最新版本 |
RR級別下,事務中的第一個SELECT請求才開始建立read view;
RC級別下,事務中每次SELECT請求都會從新建立read view;