簡介
- MVCC(Multiversion Concurrency Control),即多版本併發控制技術。它使得大部分支持行鎖的事務引擎,不再單純的使用行鎖進行數據庫的併發控制,取而代之的是把數據庫的行鎖與行的多個版本結合起來,只需要很小的開銷,就可以實現非鎖定讀(實現了讀寫併發),大大提高了數據庫的併發性能。總結一句話:MVVC就是同一份數據臨時保留多版本的一種方式,進而實現併發控制
- 功能
- 讀不阻塞寫,寫不阻塞讀(查詢和更新、刪除、插入操作互相不阻塞)
- 應對高併發事務, MVCC比
單純的加鎖
更高效 - 提供一致性讀:當開始一個查詢後,讀到的數據總是查詢開始時那個時間點的快照
- 在查詢開始後,發生的變更(即使已提交),這次查詢也是看不到的
- 一個事務無論運行多長時間,看到數據都是相同的
- 不同開始時間的事務中相同的查詢,返回的數據也可能不同
- 如果有人從數據庫中讀數據的同時,有另外的人寫入數據,有可能讀數據的人會看到不一致的數據。最簡單的方法,通過加鎖,讓所有的讀者等待寫者工作完成,但是這樣效率會很差。MVCC 使用了一種不同的手段,每個連接到數據庫的讀者,在某個瞬間看到的是數據庫的一個快照,寫者寫操作造成的變化在寫操作完成之前(或者數據庫事務提交之前)對於其他的讀者來說是不可見的。
- 當一個 MVCC 數據庫需要更新一條數據記錄的時候,它不會直接用新數據覆蓋舊數據,而是將舊數據標記爲過時並在別處增加新版本的數據。這樣就會有存儲多個版本的數據,但是隻有一個是最新的。這種方式允許讀者讀取在他讀之前已經存在的數據,即使這些在讀的過程中半路被別人修改、刪除了,也對先前正在讀的用戶沒有影響。**這種多版本的方式避免了填充刪除操作在內存和磁盤存儲結構造成的空洞的開銷,但是需要系統週期性整理(sweep through)以真實刪除老的、過時的數據。**對於面向文檔的數據庫來說,這種方式允許系統將整個文檔寫到磁盤的一塊連續區域上,當需要更新的時候,直接重寫一個版本,而不是對文檔的某些比特位、分片切除,或者維護一個鏈式的、非連續的數據庫結構。
InnoDB實現
- MVCC 是通過保存數據在某個時間點的快照來實現的。不同存儲引擎的的 MVCC 實現是不同的。典型的有樂觀併發控制和悲觀併發控制。
- InnoDB 的 MVCC 是通過在每行記錄隱藏的列來實現的。InnoDB 存儲的最基本的 row 中包含了三個隱藏字段。
- DATA_TRX_ID: 6字節,記錄了更新這行的 transaction id。InnoDB內部維護了一個遞增的tx_id_counter,其當前值可以通過
show engine innodb status
獲取 - DATE_ROLL_PTR:7字節,指向當前記錄項的 rollback segment 和 undo log,找之前版本的的數據就是通過這個指針(回滾指針)。
- DB_ROW_ID:6字節,當由 InnoDB 自動產生聚集索引時,聚集索引包括這個 DB_ROW_ID 的值,否則不包含。
- DATA_TRX_ID: 6字節,記錄了更新這行的 transaction id。InnoDB內部維護了一個遞增的tx_id_counter,其當前值可以通過
- 可見性比較的方法
- 並不是用當前事務ID與表中各個數據行上的事務ID去比較的
- 在每個事務開始的時候,會將當前系統中的所有的活躍事務拷貝到一個列表中(生成Read View,即可讀視圖),,根據Read View最早一個事務ID和最晚的一個事務ID來做比較的,這樣就能確保在當前事務之前沒有提交的所有事務的變更以及後續新啓動的事務的變更,在當前事務中都是看不到的
- 當前事務自身的變更還是需要看到的
- 與隔離級別
- MVCC只在
READ COMMITTED
和REPEATABLE READ
兩個隔離級別下工作。其他兩個隔離級別和MVCC不兼容, 因爲READ UNCOMMITTED
總是讀取最新的數據行, 而不是符合當前事務版本的數據行。而SERIALIZABLE
則會對所有讀取的行都加鎖,因此不需要MVCC
的幫助
- MVCC只在
插入
-
在MySQL中建表時,每個表都會有三列隱藏記錄,其中和MVCC有關係的有兩列
- 數據行的版本號 (DB_TRX_ID)
- 刪除版本號 (DB_ROLL_PT)
-
插入數據
begin;-- 獲取到全局事務ID insert into `test_zq` (`id`, `test_id`) values('5','68'); insert into `test_zq` (`id`, `test_id`) values('6','78'); commit;-- 提交事務
-
執行完SQL後,全局事務ID會插入到行數據的
DB_TRX_ID
列中id test_id DB_TRX_ID DB_ROLL_PT 5 68 1 NULL 6 78 1 NULL
刪除
-
對上述表格做刪除邏輯,執行以下SQL語句(假設獲取到的事務邏輯ID爲 3)
begin;--獲得全局事務ID = 3 delete test_zq where id = 6; commit;
-
執行完SQL後,數據並沒有被真正刪除,而是對刪除版本號做改變,全局事務ID會插入到行數據的
DB_ROLL_PT
列中id test_id DB_TRX_ID DB_ROLL_PT 5 68 1 NULL 6 78 1 3
修改
-
修改邏輯和刪除邏輯有點相似,修改數據的時候會先複製一條當前記錄行數據,同時標記這條數據的數據行版本號爲當前是事務版本號,最後把原來的數據行的刪除版本號標記爲當前是事務。
-
更新數據的步驟
- 用排他鎖鎖定該行
- 把該行修改前的值複製到undo log中
- 修改當前行的值,填寫事務編號,使得回滾指針指向undo log中的修改前的行
- 記錄redo log,包括undo log中的變化
- 修改成功(commit)什麼都不做,失敗則恢復undo log中的數據(rollback)
-
多次更新後,回滾指針會把不同版本的記錄串在一起。在InnoDB中存在purge線程,它會查詢那些比現在最老的活動事務還早的undo log,並刪除它們,從而保證undo log文件不會無限增長
-
執行SQL後
begin;-- 獲取全局系統事務ID 假設爲 10 update test_zq set test_id = 22 where id = 5; commit;
-
執行後表格實際數據爲
id test_id DB_TRX_ID DB_ROLL_PT 5 68 1 10 6 78 1 3 5 22 10 NULL
查詢
-
數據查詢規則如下
- 查找數據行版本號早於當前事務版本號的數據行記錄:數據行的版本號要小於或等於當前是事務的系統版本號,這樣也就確保了讀取到的數據是當前事務開始前已經存在的數據,或者是自身事務改變過的數據
- 查找刪除版本號要麼爲NULL,要麼大於當前事務版本號的記錄:這樣確保查詢出來的數據行記錄在事務開啓之前沒有被刪除
-
執行SQL
begin;-- 假設拿到的系統事務ID爲 12 select * from test_zq; commit;
-
執行結果
id test_id DB_TRX_ID DB_ROLL_PT 6 22 10 NULL