1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        一文理解MySQL的事務原則與事務隔離

        共 8434字,需瀏覽 17分鐘

         ·

        2021-06-23 12:22

        事務是一組不可分組的操作集合,這些操作要么都成功執(zhí)行,要么都取消執(zhí)行。最典型的需要事務的場景是銀行賬戶間的轉賬:假如 A 賬戶要給 B 賬戶轉賬 100 元,那么 A 賬戶要扣減 100 元,B 賬戶要增加 100 元,這兩個賬戶的數(shù)據(jù)變更都成功才可算作轉賬成功。

        本文涉及到MySQL鎖機制的很多知識,對這方面不了解的可以先看博客《快速理解MySQL的鎖機制與死鎖排查

        ACID原則

        事務具有四個原則:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability)。這四個特性簡稱為ACID原則。

        1. 原子性(Atomictiy):事務是一個包含一系列操作的原子操作。事務的原子性確保這些操作全部完成或者全部失敗。

        2. 一致性(Consistency):事務的執(zhí)行不能破壞數(shù)據(jù)庫數(shù)據(jù)的完整性和一致性,一個事務在執(zhí)行之前和執(zhí)行之后,數(shù)據(jù)庫都必須處于一致性狀態(tài)。

        如果數(shù)據(jù)庫系統(tǒng)在運行過程中發(fā)生故障,有些事務尚未完成就被迫中斷,這些未完成的事務對數(shù)據(jù)庫所作的修改有一部分已寫入物理數(shù)據(jù)庫,此時數(shù)據(jù)庫就處于不一致的狀態(tài)。(在合格的數(shù)據(jù)庫產(chǎn)品中,數(shù)據(jù)庫重啟后,會按照一定規(guī)則回滾所有導致不一致的事務)

        1. 隔離性(Isolation):數(shù)據(jù)庫允許多個并發(fā)事務同時對其數(shù)據(jù)進行讀寫和修改的能力,隔離性可以防止多個事務并發(fā)執(zhí)行時由于交叉執(zhí)行而導致數(shù)據(jù)的不一致。

        2. 持久性(Durabilit):事務處理結束后,對數(shù)據(jù)的修改就是永久的,即便系統(tǒng)故障也不會丟失。

        1. 原子性

        原子性(Atomicity)的實現(xiàn)離不開MySQL的事務日志undo log日志類型,當事務需要回滾的時候需要將數(shù)據(jù)庫狀態(tài)回滾到事務開始前,即需要撤銷所有已經(jīng)成功執(zhí)行的sql語句。undo log起了關鍵性作用。

        當事務對數(shù)據(jù)庫進行修改時,InnoDB會生成對應的undo log;如果事務執(zhí)行失敗或調(diào)用了rollback,導致事務需要回滾,便可以利用undo log中的信息將數(shù)據(jù)回滾到修改之前的樣子。

        undo log

        undo log主要為事務的回滾服務。在事務執(zhí)行的過程中,除了記錄redo log,還會記錄一定量的undo log。undo log記錄了數(shù)據(jù)在每個操作前的狀態(tài),如果事務執(zhí)行過程中需要回滾,就可以根據(jù)undo log進行回滾操作。單個事務的回滾,只會回滾當前事務做的操作,并不會影響到其他的事務做的操作。

        undo log的作用:

        1. 為了保證數(shù)據(jù)的原子性,記錄事務發(fā)生之前的一個版本,用于回滾,

        2. 通過mvcc和undo log實現(xiàn)innodb事務可重復讀和讀取已提交隔離級別。

        其中,undo log分為:

        • insert undo log:insert操作中產(chǎn)生的undo log。

        • update undo log:delete和update操作產(chǎn)生的undo log。

        1. insert操作

        因為insert操作的記錄,只對事務本身可見,對其他事務不可見。故undo log可以在事務提交后直接刪除,不需要進行purge操作,

        1. delete操作

        Delete操作在事務中并不是真正的刪除掉數(shù)據(jù)行,而是一種Delete Mark操作,在記錄上標識Delete_Bit,而不刪除記錄。是一種"假刪除",之后通過purge線程刪除。(原因是為了支持MVCC)

        1. update操作

        update分為兩種情況:update的列是否是主鍵索引。

        如果不是主鍵索引,在undo log中直接反向記錄是如何update的。即update是直接進行的。

        如果是主鍵索引,update分兩部執(zhí)行:先刪除該行,再插入一行目標行。

        與insert undo log不同的,update undo log日志,當事務提交的時候,innodb不會立即刪除undo log, 會將該事務對應的undo log放入到刪除列表中,之后通過purge線程刪除。

        因為后續(xù)還可能會用到undo log,例如隔離級別為repeatable read時的MVCC,事務讀取的都是開啟事務時的最新提交行版本,只要該事務不結束,該行版本就不能刪除(即undo log不能刪除),且undo log分配的頁可重用減少存儲空間和提升性能。

        purge線程兩個主要作用是:清理undo頁和清除page里面帶有Delete_Bit標識的數(shù)據(jù)行。

        2. 一致性

        一致性是通過事務的原子性、持久性和隔離性來保證的。

        • 原子性:語句要么全執(zhí)行,要么全不執(zhí)行,是事務最核心的特性,事務本身就是以原子性來定義的。實現(xiàn)主要基于undo log。

        • 持久性:保證事務提交后不會因為宕機等原因?qū)е聰?shù)據(jù)丟失。實現(xiàn)主要基于redo log。

        • 隔離性:保證事務執(zhí)行盡可能不受其他事務影響。InnoDB默認的隔離級別是RR,RR實現(xiàn)隔離性主要基于鎖機制(包含next-key lock)、MVCC(包括數(shù)據(jù)的隱藏列、基于undo log的版本鏈、ReadView)。

        3. 隔離性

        隔離性是指事務內(nèi)部的操作與其他事務是隔離的,詳情可以看下面章節(jié)“事務并發(fā)問題”與“解決事務并發(fā)問題”。

        4. 持久性

        持久性是指事務提交之后,所做的修改就會永久保存,不會因為系統(tǒng)故障導致數(shù)據(jù)丟失,

        redo log

        在innoDB的存儲引擎中,事務日志通過redo log和日志緩沖實現(xiàn)。事務開啟時,事務中的操作,都會先寫入存儲引擎的日志緩沖中,在事務提交之前,這些緩沖的redo log都需要提前刷新到磁盤上持久化,這就是日志先行機制(Write-Ahead Logging)。當事務提交之后,在日志緩沖中映射的數(shù)據(jù)文件才會慢慢刷新到磁盤。此時如果數(shù)據(jù)庫崩潰或者宕機,那么當系統(tǒng)重啟進行恢復時,就可以根據(jù)redo log中記錄的日志,把數(shù)據(jù)庫恢復到崩潰前的一個狀態(tài)。未完成的事務,可以繼續(xù)提交,也可以選擇回滾,這基于恢復的策略而定。

        在系統(tǒng)啟動的時候,存儲引擎就已經(jīng)為redo log分配一塊連續(xù)的存儲空間,以順序追加的方式記錄Redo Log,通過順序IO來改善性能。redo log日志的大小是固定的,即記錄滿了以后就從頭循環(huán)寫。所有的事務共享redo log的存儲空間,Redo Log按語句的執(zhí)行順序,依次交替的記錄在一起。

        緩沖的redo log刷新到磁盤上持久化,刷新頻率由innodb_flush_log_at_trx_commit變量來控制的:

        • 0 :每秒刷新寫入到磁盤中的,當系統(tǒng)崩潰,會丟失1秒鐘的數(shù)據(jù)。

        • 1:事務每次提交都寫入磁盤。

        • 2:每秒刷新寫入到磁盤中的,但跟0是有區(qū)別的。

        事務并發(fā)問題

        1. 臟讀

        臟讀是指一個事務中訪問到了另外一個事務未提交的數(shù)據(jù)。

        如果會話2更新age為 10,但是在commit之前,會話1此時獲取age,那么會獲得的值就是會話2中尚未commit的值。如果會話2再次更新了age或者執(zhí)行了rollback,而會話 1已經(jīng)拿到了age=10的值,這就是臟讀。

        2. 不可重復讀

        不可重復讀是一個事務讀取同一條記錄2次,得到的結果不一致。

        由于在讀取中間變更了數(shù)據(jù),所以會話1事務查詢期間的得到的結果就不一樣了。

        3. 幻讀

        幻讀是一個事務讀取2次,得到的記錄條數(shù)不一致。

        由于在會話1之間插入了一個新的數(shù)據(jù),所以得到的兩次數(shù)據(jù)就不一樣了。

        事務隔離級別

        為了解決上面提到的事務并發(fā)問題,SQL標準規(guī)定了四個隔離水平:

        1. 讀未提交(Read Uncommited)

        2. 讀已提交(Read Commited)

        3. 可重復讀(Repeatable Read)

        4. 串行化(Serializable)

        隔離級別臟讀不可重復讀幻讀
        讀未提交存在存在存在
        讀已提交不存在存在存在
        可重復讀不存在不存在存在
        串行化不存在不存在不存在

        1. 讀未提交(Read Uncommited)

        該隔離級別安全最低,允許臟讀。

        2. 讀已提交(Read Commited)

        存在不可重復讀取,但不允許臟讀。讀已提交只允許獲取已經(jīng)提交的數(shù)據(jù)。

        3. 可重復讀(Repeatable Read)

        保證在事務處理過程中,多次讀取同一個數(shù)據(jù)時,其值都和事務開始時刻是一致的,因此該事務級別禁止不可重復讀取和臟讀,但是有可能出現(xiàn)幻讀。

        4. 串行化(Serializable)

        是最嚴格的事務隔離級別,它要求所有事務被串行執(zhí)行,即事務只能一個接一個的進行處理,不能并發(fā)執(zhí)行。

        下面是主流數(shù)據(jù)庫的默認隔離級別:

        DatabaseDefault isolation Level
        OracleREAD_COMMITTED
        MySQLREPETABLE_READ
        Microsoft SQL ServerREAD_COMMITTED
        PostgreSQLREAD_COMMITTED
        DB2CURSOR STABILITY (a.k.a READ_COMMITTED)

        隔離級別越高,越能保證數(shù)據(jù)的完整性和一致性,但是對并發(fā)性能的影響也越大。對于多數(shù)應用程序,可以優(yōu)先考慮把數(shù)據(jù)庫系統(tǒng)的隔離級別設為Read Committed。它能夠避免臟讀取,而且具有較好的并發(fā)性能。盡管它會導致不可重復讀、幻讀和第二類丟失更新這些并發(fā)問題,在可能出現(xiàn)這類問題的業(yè)務場景,可以采用悲觀鎖或樂觀鎖來控制。

        解決事務并發(fā)問題

        MySQL、Oracle這樣的數(shù)據(jù)庫中,為了性能的考慮并不是完全按照上面的SQL標準來實現(xiàn)的。

        數(shù)據(jù)庫實現(xiàn)事務隔離的方式,基本可以分為以下兩種。

        1. 在讀取數(shù)據(jù)前,對其加鎖,阻止其他事務對數(shù)據(jù)進行修改。

        2. 不用加任何鎖,通過一定機制生成一個數(shù)據(jù)請求時間點的一致性數(shù)據(jù)快照(Snapshot),并用這個快照來提供一定級別(語句級或事務級)的一致性讀取。從用戶的角度來看,數(shù)據(jù)庫可以提供同一數(shù)據(jù)的多個版本,因此,這種技術叫做數(shù)據(jù)多版本并發(fā)控制(MultiVersion Concurrency Control,簡稱MVCC或MCC)

        MVCC

        僅通過鎖來控制實現(xiàn)事務隔離級別會存在一些問題,比如要實現(xiàn) RC(讀已提交)級別,事務a更新一行數(shù)據(jù),需要對行(實際是索引記錄)加X鎖,阻塞其它事務對該行的讀寫,事務b想要讀取該行必須等到a提交或回滾釋放鎖,這樣的話就會很大程度上限制讀寫的并發(fā)能力。

        MVCC是一種無鎖方案,用以解決事務讀-寫并發(fā)的問題,能夠極大提升讀-寫并發(fā)操作的性能。

        為了理解MVCC,需要先了解下版本鏈與ReadView,業(yè)務場景如下:

        創(chuàng)建一個表book,就三個字段,分別是主鍵book_id, 名稱book_name, 庫存stock。然后向表中插入一些數(shù)據(jù):

        INSERT INTO book VALUES(1, '數(shù)據(jù)結構', 100);
        INSERT INTO book VALUES(2, 'C++指南', 100);
        INSERT INTO book VALUES(3, '精通Java', 100);

        版本鏈

        對于使用InnoDB存儲引擎的表,其主鍵索引記錄中包含了兩個重要的隱藏列:

        1. 事務ID(DB_TRX_ID):每當事務對主鍵索引中的記錄進行修改時,都會把當前事務的事務id記錄到DB_TRX_ID中。

        2. 回滾指針(DB_ROLL_PTR):每當事務對主鍵索引中的記錄進行修改時,都會把該記錄的舊版本記錄到undo日志中,通過DB_ROLL_PTR這個指針可以用來獲取該記錄舊版本的信息。

        如果在一個事務中多次對記錄進行修改,則每次修改都會生成undo日志,并且這些undo日志通過DB_ROLL_PTR指針串聯(lián)成一個版本鏈,版本鏈的頭結點是該記錄最新的值,尾結點是事務開始時的初始值。

        例如在表book中做以下修改:

        BEGIN;
        UPDATE book SET stock = 200 WHERE id = 1;
        UPDATE book SET stock = 300 WHERE id = 1;

        那么id=1的記錄此時的版本鏈就如下圖所示:

        ReadView

        對于Read Uncommitted隔離級別的事務來說,只需要讀取版本鏈上最新版本的記錄即可;對于Serializable隔離級別的事務來說,InnoDB使用加鎖的方式來訪問記錄;而對于Read Committed和Repeatable Read隔離級別來說,都需要讀取已經(jīng)提交的事務所修改的記錄,也就是說如果版本鏈中某個版本的修改沒有提交,那么該版本的記錄時不能被讀取的。所以需要確定在Read Committed和Repeatable Read隔離級別下,版本鏈中哪個版本是能被當前事務讀取的。于是ReadView的概念被提出以解決這個問題。

        ReadView相當于某個時刻表記錄的一個快照,在這個快照中能獲取到與當前記錄相關的事務中,哪些事務是已提交的穩(wěn)定事務,哪些是正在活躍的事務,哪些是生成快照之后才開啟的事務。由此就能根據(jù)可見性比較算法判斷出版本鏈中能被讀取的最新版本記錄。

        在MySQL中,Read Committed和Repeatable Read隔離級別下的區(qū)別就是它們生成ReadView的時機不同。

        在Read Committed隔離級別下,每次讀取數(shù)據(jù)時都會生成ReadView;而在Repeatable Read隔離級別下只會在事務首次select數(shù)據(jù)時生成ReadView,之后的讀操作都會使用相同的ReadView。

        可見性比較算法

        可見性比較算法是基于事務ID的比較算法。事務id是遞增分配的,從ReadView中能獲取到生成快照時刻系統(tǒng)中活躍的事務中最小和最大的事務id(最大的事務id實際上是系統(tǒng)中將要分配給下一個事務的id值),這樣就得到一個活躍事務id的范圍,稱之為ACTIVE_TRX_ID_RANGE。那么小于這個范圍的事務id對應的事務都是已提交的穩(wěn)定事務,大于這個范圍的事務都是在快照生成之后才開啟的事務,而在ACTIVE_TRX_ID_RANGE范圍內(nèi)的事務中除了正在活躍的事務,也都是已提交的穩(wěn)定事務。

        具體的可見性比較算法過程如下:

        1. 首先判斷版本記錄的DB_TRX_ID字段與ReadView的creator_trx_id字段是否相等。如果相等,那就說明該版本的記錄是在當前事務中生成的,自然也就能夠被當前事務讀??;否則進行第2步。

        2. 如果版本記錄的DB_TRX_ID字段小于范圍ACTIVE_TRX_ID_RANGE,表明該版本記錄是已提交事務修改的記錄,即對當前事務可見;否則進行下一步。

        3. 版本記錄的DB_TRX_ID字段位于范圍ACTIVE_TRX_ID_RANGE內(nèi):如果該事務ID對應的不是活躍事務(已經(jīng)提交了),即對當前事務可見;如果該事務ID對應的是活躍事務(還未提交了),那么對當前事務不可見,則讀取版本鏈中下一個版本記錄。重復以上步驟,直到找到對當前事務可見的版本。

        creator_trx_id字段:ReadView中的屬性,記錄創(chuàng)建這條記錄/最后一次修改該記錄的事務ID。

        如果某個版本記錄經(jīng)過以上步驟判斷確定其對當前事務可見,則查詢結果返回此版本記錄;否則讀取下一個版本記錄繼續(xù)按照上述步驟進行判斷,直到版本鏈的尾結點。如果遍歷完版本鏈沒有找到對當前事務可見的版本,則查詢結果為空。

        Read Committed下的MVCC

        假設在Read Committed隔離級別下,有如下事務在執(zhí)行,事務id為10:

        BEGIN; // 開啟Transaction 10
        UPDATE book SET stock = 200 WHERE id = 2;
        UPDATE book SET stock = 300 WHERE id = 2;

        此時該事務尚未提交,id為2的記錄版本鏈如下圖所示:

        然后開啟一個事務對id為2的記錄進行查詢:

        BEGIN;
        SELECT * FROM book WHERE id = 2; // 此時Transaction 10 未提交

        當執(zhí)行SELECT語句時會生成一個ReadView,該ReadView中的ACTIVE_TRX_ID_RANGE為[10, 11),creator_trx_id為0(因為事務中當執(zhí)行寫操作時才會分配一個單獨的事務id,否則事務id為0)。按照之前所述ReadView的工作原理,查詢到的id=2的book,stock值應為100。

        然后將事務id為10的事務提交:

        BEGIN; // 開啟Transaction 10
        UPDATE book SET stock = 200 WHERE id = 2;
        UPDATE book SET stock = 300 WHERE id = 2;
        COMMIT;

        同時開啟執(zhí)行另一事務id為11的事務,但不提交:

        BEGIN; // 開啟Transaction 11
        UPDATE book SET stock = 400 WHERE id = 2;

        此時id為2的記錄版本鏈如下圖所示: 

        然后回到剛才的查詢事務中再次查詢id為2的記錄:

        BEGIN;
        SELECT * FROM book WHERE id = 2; // 此時Transaction 10 未提交
        SELECT * FROM book WHERE id = 2; // 此時Transaction 10 已提交

        當?shù)诙螆?zhí)行SELECT語句時會再次生成一個ReadView,該ReadView中的ACTIVE_TRX_ID_RANGE為[11, 12),當前事務IDcreator_trx_id依然為0。按照ReadView的工作原理進行分析,查詢到的id=2的book,stock值為300。

        從上述分析可以發(fā)現(xiàn),因為每次執(zhí)行查詢語句都會生成新的ReadView,所以在Read Committed隔離級別下的事務讀取到的是查詢時刻表中已提交事務修改之后的數(shù)據(jù)。

        Repeatable Read下的MVCC

        Repeatable Read隔離級別下的事務只會在第一次執(zhí)行查詢時生成ReadView,該事務中后續(xù)的查詢操作都不會生成新的ReadView,因此Repeatable Read隔離級別下,一個事務中多次執(zhí)行同樣的查詢,其結果都是一樣的,這樣就實現(xiàn)了可重復讀。

        補充

        redo/undo log和binlog的區(qū)別:

        1. 層次不同。redo/undo是innodb引擎層維護的,而binlog是mysql server層維護的,跟采用何種引擎沒有關系,記錄的是所有引擎的更新操作的日志記錄。

        2. 記錄內(nèi)容不同。redo/undo記錄的是每個頁/每個數(shù)據(jù)的修改情況,屬于物理日志+邏輯日志結合的方式(redo log是物理日志,undo log 是邏輯日志);binlog是邏輯日志,其記錄是對應的SQL語句。

        3. 記錄時機不同。redo/undo在事務執(zhí)行過程中會不斷的寫入,而binlog是在事務最終提交前寫入的。binlog什么時候刷新到磁盤跟參數(shù)sync_binlog相關。

        當前讀

        SELECT xxx LOCK IN SHARE MODE 語句為當前讀,加S鎖和元數(shù)據(jù)讀鎖,直到事務結束。

        SELECT xxx FOR UPDATE 語句為當前讀,加X鎖和元數(shù)據(jù)讀鎖,直到事務結束

        這種讀取方式讀取的是記錄的當前最新版本,稱為當前讀。另外對于INSERT、DELETE、UPDATE操作,也需要先讀取記錄,獲取記錄的X鎖,這個過程也是一個當前讀。由于需要對記錄進行加鎖,會阻塞其他事務的寫操作,因此也叫加鎖讀或阻塞讀。

        在MySQL的Repeatable Read隔離級別下,當前讀使用臨鍵鎖解決幻讀問題。

        快照讀

        在Read Committed和Repeatable Read隔離級別下,普通的SELECT查詢(不包括 select ... lock in share mode, select ... for update)都是讀取MVCC版本鏈中的一個版本,相當于讀取一個快照,因此稱為快照讀。這種讀取方式不會加鎖,因此讀操作時非阻塞的,也叫非阻塞讀。

        在標準的Repeatable Read隔離級別下讀操作會加S鎖,直到事務結束,因此可以阻止其他事務的寫操作;但在MySQL的Repeatable Read隔離級別下讀操作沒有加鎖,不會阻止其他事務對相同記錄的寫操作。

        注意:快照讀的時間點是以第一個select來確認的。所以即使事務開始后先update再select,那么select獲取的數(shù)據(jù)則是對應update動作之后生成的ReadView。

        在MySQL的Repeatable Read隔離級別下,快照讀使用MVCC解決幻讀問題。


        瀏覽 70
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            日本边添边摸边做边爱的导演介绍 | 欧美BBw搡BBBB搡BBBB | 看逼网址 | 人人爱夜夜爽日日视频 | 啪啪啪性爱网站 | 欧美日韩亚洲色图 | 波多野结衣喷水视频 | heyzo精品少妇 | 航空裸体服务特级毛片 | 日韩精品黄色电影 |