MySQL多版本並發控制MVCC實例分析

PHPz
發布: 2023-06-03 11:51:02
轉載
818 人瀏覽過

    1.什麼是MVCC

    MVCC (Multiversion Concurrency Control),多版本並發控制。顧名思義,MVCC是透過資料行的多個版本管理來實現資料庫的並發控制。這項技術使得在InnoDB的交易隔離等級下執行一致性讀取.操作有了保證。換言之,就是為了查詢一些正在被另一個事務更新的行,並且可以看到它們被更新之前的值,這樣在做查詢的時候就不用等待另一個事務釋放鎖定。

    MVCC沒有正式的標準,在不同的DBMS中MVCC的實作方式可能是不同的,也不是普遍使用的(大家可以參考相關的DBMS文件)。這裡講解InnoDB中MVCC的實作機制(MySQL其它的儲存引擎並不支援它)

    2快照讀取與目前讀取

    MVCC在MySQL InnoDB中的實作主要是為了提高資料庫並發效能,用更好的方式去處理讀-寫衝突,做到即使有讀寫衝突時,也能做到不加鎖非阻塞並發讀,而這個讀指的就是快照讀,而不是目前讀。目前讀取其實是一種加鎖的操作,是悲觀鎖的實現。而MVCC本質是採用樂觀鎖思想的一種方式。

    2.1 快照讀

    快照讀又叫一致性讀,讀取的是快照資料。 不加鎖的簡單的SELECT都屬於快照讀即不加鎖的非阻塞讀;比如這樣:

    select * from player where ...
    登入後複製

    之所以出現快照讀的情況,是基於提高並發效能的考慮,快照讀取的實作是基於MVCC,它在許多情況下,避免了加鎖操作,降低了開銷。

    既然是基於多個版本,那麼快照讀取可能讀到的並不一定是資料的最新版本,而有可能是先前的歷史版本。

    快照讀取的前提是隔離級別不是串行級別,串行級別下的快照讀取會退化成當前讀取。

    2.2目前讀

    目前讀取讀取的是記錄的最新版本(最新數據,而不是歷史版本的數據),讀取時還要保證其他並發事務不能修改當前記錄,會對讀取的記錄進行加鎖。加鎖的SELECT,或資料增刪改都會進行目前讀取。 例如:

    MySQL多版本並發控制MVCC實例分析

    3.複習

    3.1 再談隔離等級

    我們知道交易有4個隔離級別,可能有三種並發問題:

    MySQL多版本並發控制MVCC實例分析

    在MysQL 中,預設的隔離等級是可重複讀,可以解決髒讀和不可重複讀的問題,如果僅從定義的角度來看,它並不能解決幻讀問題。如果我們想要解決幻讀問題,就需要採用串列化的方式,也就是將隔離等級提升到最高,但這樣一來就會大幅降低資料庫的事務並發能力。

    MVCC可以不採用鎖定機制,而是透過樂觀鎖的方式來解決不可重複讀取和幻讀問題!它可以在大多數情況下替代行級鎖,降低系統的開銷。

    MySQL多版本並發控制MVCC實例分析

    3.2 隱藏欄位、Undo Log版本鏈

    回顧undo日誌的版本鏈,對於使用InnoDB儲存引擎的表來說,它的聚簇索引記錄中都包含兩個必要的隱藏列。

    trx_id:每次一個交易對某條叢集索引記錄進行改變時,都會把該交易的交易id賦值給trx_id隱藏欄位。 roll_pointer:每次對某條聚簇索引記錄進行改動時,都會把舊的版本寫入到undo日誌中,然後這個隱藏列就相當於一個指針,可以透過它來找到該記錄修改前的信息。

    MySQL多版本並發控制MVCC實例分析

    MySQL多版本並發控制MVCC實例分析

    MySQL多版本並發控制MVCC實例分析

    MySQL多版本並發控制MVCC實例分析

    #4、MVCC實作原理之ReadView

    MVCC的實作依賴:隱藏欄位、Undo Log、Read View

    4.1什麼是ReadView

    在MVCC機制中,多個事務對同一行記錄進行更新會產生多個歷史快照,這些歷史快照保存在Undo Log裡。如果一個事務想要查詢這個行記錄,需要讀取哪個版本的行記錄呢?這時就需要用到ReadView了,它幫我們解決了行的可見性問題。

    當交易使用MVCC機制進行快照讀取操作時,會產生一個讀取視圖,而這個視圖就是ReadView。當事務啟動時,會產生資料庫系統目前的快照,InnoDB為每個事務建構了一個數組,用來記錄並維護系統當前活躍事務的lD(“"活躍"指的就是,啟動了但還沒提交)。

    4.2 設計想法

    使用READ UNCOMMITTED隔離等級的事務,由於可以讀到未提交事務修改過的記錄,所以直接讀取記錄的最新版本就好了。

    使用SERIALIZABLE隔離等級的事務,InnoDB規定使用加鎖的方式來存取記錄。

    使用READ COMMITTEDREPEATABLE READ隔離層級的事務,都必須保證讀到已經提交了的事務修改過的記錄。假如另一個事務已經修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的,核心問題就是需要判斷版本鏈中的哪個版本是當前事務可見的,這是ReadView要解決的主要問題。

    這個ReadView中主要包含4個比較重要的內容,分別如下:

    MySQL多版本並發控制MVCC實例分析

    4.3 ReadView的規則

    有了這個ReadView,這樣在存取某筆記錄時,只需要依照下邊的步驟判斷記錄的某個版本是否可見。

    • 如果被存取版本的trx_id屬性值與ReadView中的creator_trx_id#值相同,意味著目前事務在存取它自己修改過的記錄,所以該版本可以被目前事務存取。

    • #如果被存取版本的trx_id屬性值小於ReadView中的up_limit_id值,表示產生該版本的事務在目前事務產生ReadView前已經提交,所以該版本可以被目前事務存取。

    • 如果被存取版本的trx_id屬性值大於或等於ReadView中的low_limit_id值,表示產生該版本的事務在目前事務產生ReadView後才開啟,所以該版本不可以被目前事務存取。

    • 如果被存取版本的trx_id屬性值在ReadView的up_limit_idlow_limit_id之間,那就需要判斷trx_id屬性值是不是在trx_ids清單中。如果在,說明建立ReadView時產生該版本的事務還是活躍的,則該版本不可以被存取。如果不在,說明在建立ReadView時產生該版本的交易已經被提交,該版本可以被存取。 4.4 MVCC整體操作流程

    了解了這些概念之後,我們來看下當查詢一筆記錄的時候,系統如何透過MVCC找到它:

    • 1.先取得交易自己的版本號,也就是交易ID;

    • 2.取得ReadView;

    • 3.查詢所得到的數據,然後與ReadView中的事務版本號進行比較;

    • #4.如果不符合Readview規則,就需要從Undo Log取得歷史快照;

    • #5.最後傳回符合規則的資料。

    如果某個版本的數據對當前事務不可見的話,那就順著版本鏈找到下一個版本的數據,繼續按照上邊的步驟判斷可見性,依此類推,直到版本鏈中的最後一個版本。如果最近版本不可見,則該記錄對該交易不可見,且查詢結果中不包含該記錄。在

    InnoDB中,MVCC是透過Undo Log Read View進行資料讀取,Undo Log保存了歷史快照,而Read View規則幫我們判斷目前版本的資料是否可見。

    在隔離層級為讀取已提交(Read Committed)時,一個交易中的每一次select查詢都會重新取得一次Read View。

    MySQL多版本並發控制MVCC實例分析

    當隔離等級為可重讀的時候,就避免了不可重複讀,這是因為一個交易只在第一次select的時候會取得一次Read View,而後面所有的select都會重複使用這個Read View,

    如下:

    MySQL多版本並發控制MVCC實例分析

    5.舉例說明

    假設現在student表中只有一筆由交易id8交易插入一筆記錄:

    MySQL多版本並發控制MVCC實例分析

    ##MVCC只能在READ COMMITTED和REPEATABLE READ兩個隔離等級下運作。接下來看一下

    READ COMMITTEDREPEATABLE READ所謂的生成Readview的時機不同到底不同在哪裡。

    5.1 READ COMMITTED

    #隔離等級下:

    READ COMMITTED:每次讀取資料前都會產生一個ReadView

    現在有兩個交易id分別為10、20的交易在執行:

    MySQL多版本並發控制MVCC實例分析

    說明:交易執行在過程中,只有在第一次真正修改記錄時(例如使用INSERT、DELETE、UPDATE語句),才會被指派一個單獨的事務id,這個事務id是遞增的。所以我們才在事務2中更新一些別的表的記錄,目的是讓它分配事務id。

    哈哈此刻,表student中id為1的記錄所得到的版本鍊錶如下所示:

    MySQL多版本並發控制MVCC實例分析

    假設現在有一個使用READ COMMITED隔離等級的交易開始執行:

    MySQL多版本並發控制MVCC實例分析

    #這個SELECT1的執行過程如下:

    步驟1∶在執行SELECT語句時會先生成一個ReadView ,ReadView的trx_ids列表的內容就是[10,20],up_limit_id10, low_limit_id21 , creator_trx_id0
    步驟2:從版本鏈中挑選可見的記錄,從圖中看出,最新版本的列name的內容是'王五',該版本的trx_id值為10,在trx_ids清單內,所以不符合可見性要求,根據roll_pointer跳到下一個版本。
    步驟3:下一個版本的欄位name的內容是'李四',該版本的trx_id值也是10,也在trx_ids清單內,所以也不符合要求,繼續跳到下一個版本。
    步驟4:下一個版本的欄位name的內容是‘張三',該版本的trx_id值為8,小於ReadView 中的up_limit_id值10,所以這個版本是符合要求的,最後回傳給使用者的版本就是這條列name為‘張三’的記錄。

    之後,我們把交易id10的交易提交一下:

    MySQL多版本並發控制MVCC實例分析

    ##然後到

    事務id20的事務中更新一下表格studentid1的記錄:

    MySQL多版本並發控制MVCC實例分析

    此時,表student中id為1的記錄的版本鏈就長這樣:

    MySQL多版本並發控制MVCC實例分析

    然後再到剛才使用READ COMMITTED隔離等級的交易中繼續找出id為1的記錄,

    如下:

    MySQL多版本並發控制MVCC實例分析

    ##這個SELECT2的執行流程如下:

    步驟1∶

    在執行SELECT語句時會又會單獨產生一個ReadView,該ReadView的trx_ids列表的內容就是[20],up_limit_id為20, low_limit_id為21, creator_trx_id為0。 步驟2:
    從版本鏈中挑選可見的記錄,從圖中看出,最新版本的列name的內容是‘宋八’,該版本的tr×_id值為20,在trx_ids列表內,所以不符合可見性要求,根據roll.pointer跳到下一個版本。 步驟3∶
    下一個版本的列name的內容是’錢七’,該版本的trx_id值為20,也在trx_ids列表內,所以也不符合要求,繼續跳到下一個版本。 步驟4∶下一個版本的列name的內容是’王五’,該版本的trx_id值為10,小於ReadView中的up_limit.id值20,所以這個版本是符合要求的,最後返回給使用者的版本就是這條列name為’王五’的記錄。 以此類推,如果之後事務id為20的記錄也提交了,再次在使用READ CONMMITTED隔離級別的事務中查詢表student中id值為1的記錄時,得到的結果就是‘宋八&rsquo ;了,具體流程我們就不分析了。

    5.2 REPEATABLE READ

    在隔離等級下:

    使用REPEATABLE READ隔離等級的交易來說,只會在第一次執行查詢語句時產生一個ReadView,之後的查詢就不會重複產生了。

    例如,系統裡有兩個交易id分別為10、20的交易在執行:

    此刻,表student中id为1的记录得到的版本链表如下所示:

    MySQL多版本並發控制MVCC實例分析

    假设现在有一个使用REPEATABLE READ隔离级别的事务开始执行:

    MySQL多版本並發控制MVCC實例分析

    此时执行过程与read committed相同

    MySQL多版本並發控制MVCC實例分析

    MySQL多版本並發控制MVCC實例分析

    然后再到刚才使用REPEATABLE READ隔离级别的事务中继续查找id为1的记录,如下:

    MySQL多版本並發控制MVCC實例分析

    这个SELECT2的执行过程如下:

    步骤1:因为当前事务的隔离级别为REPEATABLE READ,而之前在执行SELECT1时已经生成过ReadView了,所以此时直接复用之前的ReadView,之前的ReadView的trx_ids列表的内容就是[10,20],up_limit_id为10, low_limit_id为21 , creator_trx_id为0。
    步骤2:然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name的内容是’宋八’trx_id值为20,在trx_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
    步骤3:下一个版本的列name的内容是’钱七’,该版本的trx_id值为20,也在trx_ids列表内合要求,继续跳到下一个版本。
    步骤4:下一个版本的列name的内容是’王五’,该版本的trx_id值为10,而trx_ids列表中是包含值为10的事务id的,所以该版本也不符合要求,同理下一个列name的内容是’李四’的版本也不符合要求。继续跳到下个版本。
    步聚5∶下一个版本的列name的内容是’张三’,该版本的trx_id值为80,小于Readview中的up_limit_id值10,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c为‘张三’的记录。
    两次SELECT查询得到的结果是重复的,记录的列c值都是’张三’,这就是可重复读的含义。如果我们之后再把事务id为20的记录提交了,然后再到刚才使用REPEATABLE READ隔离级刷的事务中继续查找这个id为1的记录,得到的结果还是’张三’,具体执行过程大家可以自己分析一下。

    5.3 如何解决幻读

    假设现在表student中只有一条数据,数据内容中,主键id=1,隐藏的trx_id=10,它的undo log如下图所示。

    MySQL多版本並發控制MVCC實例分析

    假设现在有事务A和事务B并发执行,事务A的事务id为20,事务B的事务id为30。
    步骤1:事务A开始第一次查询数据,查询的SQL语句如下。

    select * from student where id > 1;
    登入後複製

    在开始查询之前,MySQL会为事务A产生一个ReadView,此时ReadView的内容如下: trx_ids=[20, 30 ] ,up_limit_id=20 , low_limit_id=31 , creator_trx_id=20。

    因为表student只有一条符合条件 where id>=1 的数据,所以会被查询出来。然后根据ReadView机制,发现该行数据的trx_id=10,小于事务A的ReadView里up_limit_id,这表示这条数据是事务A开启之前,其他事务就已经提交了的数据,因此事务A可以读取到。

    结论:事务A的第一次查询,能读取到一条数据,id=1。

    步骤2∶接着事务B(trx_id=30),往表student中新插入两条数据,并提交事务。

    insert into student(id,name) values(2,'李四');
    insert into student(id,name) values(3,'王五');
    登入後複製

    此时表student中就有三条数据了,对应的undo如下图所示:

    MySQL多版本並發控制MVCC實例分析

    步驟3∶接著事務A開啟第二次查詢,根據可重複讀取隔離等級的規則,此時事務A並不會再重新產生ReadView。此時表student中的3個資料都滿足 where id>=1的條件,因此會先查出來。然後根據ReadView機制,判斷每個資料是不是都可以被事務A看到。
    1)首先 id=1的這條數據,前面已經說過了,可以被事務A看到。
    2)接著是id=2的數據,它的trx_id=30,此時事務A發現,這個值處於up_limit_id和low_limit_id之間,因此還需要再判斷30是否處於trx_ids數組內。由於事務A的trx_ids=[20,30],因此在數組內,這表示id=2的這條資料是與事務A在同一時刻啟動的其他事務提交的,所以這條資料不能讓事務A看到。
    3)同理,id=3的這條資料, trx_id 也是30,因此也不能被事務A看見。

    MySQL多版本並發控制MVCC實例分析

    結論:最終交易A的第二次查詢,只能查詢出id=1的這條資料。這和事務A的第一次查詢的結果是一樣的,因此沒有出現幻讀現象,所以說在 MySQL的可重複續隔離等級下,不存在幻讀問題。

    以上是MySQL多版本並發控制MVCC實例分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    相關標籤:
    來源:yisu.com
    本網站聲明
    本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
    熱門教學
    更多>
    最新下載
    更多>
    網站特效
    網站源碼
    網站素材
    前端模板
    關於我們 免責聲明 Sitemap
    PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!