MySQL的MVCC


MySQL的MVCC

轉載地址

一、什么是MVCC

MVCC是Multi-Version Concurrency Control的簡稱,即多版本並發控制。MVCC是現代數據庫引擎實現中常用的處理讀寫沖突的手段,目的在於提高數據庫高並發場景下的吞吐性能。如此一來不同的事務在並發過程中,select操作可以不加鎖而是通過MVCC機制讀取指定的版本歷史記錄,並通過一些手段保證讀取的記錄值符合事務所處的隔離級別,從而解決並發場景下的讀寫沖突。

下面舉一個多版本讀的例子,例如兩個事務A和B按照如下順序進行更新和讀取操作

transaction A transaction B
select x from table; return 10
begin transaction
update table set x=20
begin transaction
select x from table ; return rs1
commit
select x from table ; return rs2
commit

在事務A提交前后,事務B讀取到的X的值是什么呢?答案是:事務B在不同的隔離級別下,讀取到的值不一樣。

  • 如果事務B的隔離級別是讀未提交,那么兩次讀取均讀取到X的最新值,即20。
  • 如果事務B的隔離級別是讀已提交,那么第一次讀取到的是舊值10,第二次因為事務A已經提交,則讀取到新值20。
  • 如果事務B的隔離級別是可重復讀或者串行,則兩次均讀到舊值10,不論事務A是否已經提交

二、為什么需要MVCC

InnoDB相比MyISAM有兩大特點,一是支持事務二是支持行級鎖,事務的引入帶來了一些新的挑戰。相對於串行處理來說,並發事務處理能大大增加數據庫資源的利用率,提高數據庫系統的事務吞吐量,從而可以支持更多的用戶。但並發事務處理也會帶來一些問題,主要包括一下幾種情況:

  1. 更新丟失:當兩個或多個事務選擇同一行,然后基於最初選定的值更新該行時,由於每個事務都不知道其他事務的存在,就會發生丟失更新的問題-- 最后的更新覆蓋了其他事務所做的更新。如何避免這個問題呢?最好在一個事務對數據進行更改但還未提交時,其他事務不能訪問修改同一個數據。
  2. 臟讀:一個事務正在對一條記錄做修改,在這個事務提交前,這時,另一個事務夜來讀取同一條記錄並且讀到了修改后尚未提交的數據,並依據此做了進一步的處理。這種現象就被形象的叫做臟讀
  3. 不可重復讀:指在一個事務內,多次讀同一數據,前后讀取的結果不一致。
  4. 幻讀:幻讀是指當事務不是獨立執行時發生的一種現象,例如事務A對表中的一個數據進行了修改,這種修改涉及到表中的全部數據行。同時事務B也修改了這個表中的數據,這種修改是向表中插入一行新數據。那么就會發生操作事務A的用戶發現表中還存在沒有修改的數據行,就好像發生了幻覺一樣。

以上是並發事務過程中會存在的問題,解決更新丟失可以交給應用,但是后三者需要數據庫提供事務間的隔離機制來解決。實現隔離機制的方法主要有兩種:

  1. 加讀寫鎖
  2. 一致性快照讀,即MVCC

本質上,隔離級別是一種在並發性能和並發產生的副作用間的妥協,通常數據庫均傾向於后者采用 weak isolation。

三、InnoDB MVCC實現原理

InnoDB中MVCC的實現方式為:每一行記錄都有兩個隱藏列:DATA_TRX_ID、DATA_ROLL_PTR(如果沒有主鍵,則還會多一個隱藏的主鍵列)。

image.png
DATA_TRX_ID:記錄最近更新這條記錄的事務ID,大小為6字節
DATA_ROLL_PTR:表示指向該行回滾段(rollback segment)的指針,大小為7個字節,InnoDB便是通過這個指針找到之前版本的數據。該行記錄上所有舊版本,在undo log中通過鏈表的形式組織。
DB_ROW_ID:行標識(隱藏自增ID),大小為6字節,如果表沒有主鍵,InnoDB會自動生成一個隱藏主鍵,因此會出現這個列。另外,每條記錄的頭信息(record header)里都有一個專門的bit(deleted_flag)來表示當前記錄是否已經被刪除。

undo log

MySQL中有六種日志文件。分別是重做日志(redo log)、回滾日志(undo log)、二進制日志(bin log)、錯誤日志(error log)、慢查詢日志(slow query log)、一般查詢日志(general log)。其中redo log、undo log、bin log與事務操作息息相關。

undo log是將用戶上一步做的操作對程序造成的改動恢復到改動之前,和redo log的區別是redo log是指重新實現這種改動。

3.1版本鏈

在多個事務並行操作某行數據的情況下,不同事務對該行數據的update會產生多個版本,然后通過回滾指針組織成一條 undo log鏈,下面我們通過一個簡單的例子來看一下 undo log 鏈是如何組織的,DATA_TRX_ID和DATA_ROLL_PTR兩個參數在其中又起到什么樣的作用。

還以上文MVCC的例子,事務A對值X進行更新之后,該行即產生一個新版本和舊版本。假設之前插入該行的事務ID為100,事務A的ID為200,該行的隱藏主鍵為1。
image.png

事務A的操作過程為:

  1. 對DB_ROW_ID=1的這行記錄加排他鎖
  2. 把該行原本的值拷貝到undo log中,DB_TRX_ID和DB_ROLL_PTR都不動
  3. 修改該行的值這時產生一個新版本,更新DATA_TRX_ID為修改記錄的事務ID,將DATA_ROLL_PRT指向剛剛拷貝到undo log鏈中的舊版本記錄,這樣就能通過DATA_ROLL_PTR找到這條記錄的歷史版本。如果對同一行記錄執行連續的update,undo log會組成一個鏈表,遍歷這個鏈表可以看到這條記錄的變遷
  4. 記錄redo log,包括undo log中的修改

那么insert和delete會怎么做呢?其實相比update這兩者很簡單,insert會產生一條新記錄,它的DATA_TRX_ID為當前插入記錄的事務ID;delete某條記錄時可看成是一種特殊的update,其實是軟刪,真正執行刪除操作會在commit時,DATA_TRX_ID則記錄下刪除該記錄的事務ID。

這里還有一個問題就是,當前事務在啟動時看到的內容是哪個版本的?這里就需要視圖 read-view了。而且不同時刻啟動的事務會有不同的read-view。

3.2如何實現一致性讀 ReadView

在讀未提交隔離級別下,直接讀取新版本的記錄了;在串行化隔離級別下,通過加鎖互斥來訪問數據,因此不需要MVCC的幫助。因此MVCC運行在讀提交、可重復讀 這兩個隔離級別下,當InnoDB隔離級別設置為二者其一時,就會用到版本鏈。

那現在的問題就是哪些版本是對當前啟動的事務可見?為了解決這個問題,於是有了ReadView(可讀視圖)的概念

3.2.1可重復讀隔離級別下ReadView的生成

在可重復讀隔離級別下,如果是通過begin啟動的事務,那么當該事務執行第一個sql語句時,生成ReadView,即會將當前系統中的所有活躍的事務拷貝到一個列表生成ReadView。

下圖事務A第一條select語句在事務B更新數據前,因此生成的ReadView在事務A過程中不發生變化,即使事務B在事務A之前提交,但是事務A第二條查詢語句依舊無法讀到事務B的修改。

transaction A transaction B
可重復讀隔離級別
set tx_isolation=repeatable-read 可重復讀隔離級別
set tx_isolation=repeatable-read
begin transaction begin transaction
select x from table ; return 10
這個時候生成ReadView
update table set x=20
commit
select x from table; return 10
無法讀到x=20
commit

下圖中,事務A的第一條sql語句在事務B的修改提交之后,因此可以讀到事務B的修改。但是注意,如果事務A的第一條select語句查詢時,事務B還沒有提交,那么事務A也查不到事務B的修改。

transaction A transaction B
可重復讀隔離級別
set tx_isolation=repeatable-read 可重復讀隔離級別
set tx_isolation=repeatable-read
begin transaction begin transaction
update table set x=20
commit
select x from table ; return 20
這個時候讀到了事務B的修改x=20
commit

3.2.2讀提交隔離級別下ReadView的生成

在讀提交隔離級別下,每個sql語句(select查詢語句)開始時,都會重新將當前系統中的活躍事務拷貝一個列表生成ReadView。二者的區別就在於生成ReadView的時間點不同,一個是事務之后第一個查詢語句開始;一個是事務中每條select語句開始。

3.3 ReadView列表

ReadView中當前活躍的事務ID列表,稱為m_ids,其中最小值為up_limit_id,最大值為low_limit_id,事務ID是事務開啟時InnoDB分配的,其大小決定了事務開啟的先后順序,因此我們可以通過ID的大小關系來決定版本記錄的可見性,其判斷流程如下:

  1. 如果被訪問版本的trx_id小於m_ids中的最小值up_limit_id,說明生成該版本的事務在ReadView生成之間已經提交了,所以該版本可以被當前事務訪問。
  2. 如果被訪問版本的trx_id大於m_ids中的最大值low_limit_id,說明生成該版本的事務在ReadView生成之后才生成,所以該版本不可以被當前事務訪問。
  3. 如果被訪問版本的trx_id在m_ids列表中最大值和最小值之間,那就需要判斷一下trx_id是不是在m_ids列表中。如果在,說明創建ReadView時生成該版本所屬事務還是活躍的,因此該版本不可以被訪問,需要查找undo log鏈找到上一個版本,然后根據該版本的data_trx_id在從頭計算一次可見性;如果不在說明創建ReadView時生成該版本的事務已經提交,該版本可以被訪問
  4. 此時經過一系列的判斷我們已經得到了這條記錄相對ReadView來說的可見結果。此時,如果這條記錄的delete_flag為true,說明這條記錄已經被刪除,不返回。否則說明這條記錄可以安全返回給客戶端。

四、舉個例子

4.1 讀提交下的MVCC判斷流程

我們現在回看剛剛的查詢過程,為什么事務B在讀提交隔離級別下,兩次查詢X值不同。讀提交隔離級別下 ReadView是在語句顆粒度上生成的。

當事務A未提交時,事務B進行查詢,假設事務B的事務ID為300,此時生成ReadView的m_ids為[200,300],而最新版本的trx_id為200,處於m_ids中,則該版本不可被訪問,查詢版本鏈得到上一條記錄的trx_id為100,小於m_ids的最小值200,因此可以被訪問,此時事務B就查詢到值10而非20。

待事務A提交之后,事務B進行查詢,此時生成的ReadView的m_ids為[300],而最新的版本記錄中trx_id為200,小於m_ids的最小值300,因此可以被訪問到,此時事務B就查詢到20。

4.2 可重復讀下的MVCC判斷流程

如果在可重復讀隔離級別下,為什么事務B前后兩次均查詢到10呢?可重復讀下生成ReadView是事務開始時,m_ids為[200,300],后面不發生變化,因此即使事務A提交了,trx_id為200的記錄依舊處於m_ids中,不能被訪問,只能訪問版本鏈中的記錄10。

五、寫在最后

讀提交、可重復讀兩種隔離級別的事務在執行普通的讀操作時,通過訪問版本鏈的方法,使得事務間的讀寫操作得以並發執行,從而提升系統性能。讀提交、可重復讀這兩個隔離級別的一個很大不同就是生成ReadView的時間點不同,讀提交在每一次select語句前都會生成一個ReadView,事務期間會更新,因此在其他事務提交前后所得到的m_ids列表可能發生變化,使得先前不可見的版本后續又突然可見了;而可重復讀只在事務的第一個select語句時生成一個ReadView,事務操作期間不更新。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM