數(shù)據(jù)庫事務(wù)

事務(wù)特性

  • Atomicity(原子性)
    一個事務(wù)必須被視為一個不可分割的最小工作單位,整個事務(wù)中的所有操作要么全部提交成功,要么全部失敗回滾。
  • Consistency(一致性)
    數(shù)據(jù)庫總是從一個一致性狀態(tài)轉(zhuǎn)換到另一個一致性狀態(tài),事務(wù)執(zhí)行之前和執(zhí)行之后都必須處于一致性狀態(tài)。
  • Isolation(隔離性)
    通常來說,一個事務(wù)所做的修改在最終提交之前,對其它事務(wù)是不可見的。關(guān)于事務(wù)的隔離性,數(shù)據(jù)庫提供了多種隔離級別。
  • Durability(持久性)
    一旦事務(wù)提交,則其所做的修改就會永久保存到數(shù)據(jù)庫中。即便是數(shù)據(jù)庫系統(tǒng)遇到故障的情況下也不會丟失。

并發(fā)事務(wù)的問題

  • 臟讀
    一個事務(wù)正在對一條記錄進行修改,在這個事務(wù)完成并提交前, 這條記錄的數(shù)據(jù)就處于不一致狀態(tài)。這時, 另一個事務(wù)也來讀取同一條記錄,如果不加控制,第二個事務(wù)讀取了這些“臟”數(shù)據(jù),并據(jù)此做進一步的處理,就會產(chǎn)生未提交的數(shù)據(jù)依賴關(guān)系。
    時間 事務(wù)A 事務(wù)B
    T1 開啟事務(wù) 開啟事務(wù)
    T2 查詢賬戶余額為1000
    T3 充值500,余額修改為1500
    T4 查詢余額為1500
    T5 撤銷事務(wù),余額改回1000
    T6 匯入500,余額修改為2000
    T7 提交事務(wù)
  • 不可重復(fù)讀
    一個事務(wù)在讀取某些數(shù)據(jù)后的某個時間,再次讀取以前讀過的數(shù)據(jù),卻發(fā)現(xiàn)其讀出的數(shù)據(jù)已經(jīng)發(fā)生了變更、或者某些記錄已經(jīng)被刪除了。
    時間 事務(wù)A 事務(wù)B
    T1 開啟事務(wù) 開啟事務(wù)
    T2 select * from user where user_id=100 假設(shè)為小明的用戶信息
    T3 將user_id=100的用戶信息對應(yīng)的年齡修改為18
    T4 提交事務(wù)
    T5 再次查詢發(fā)現(xiàn)用戶的年齡變更
    T6 ...
    T7 提交事務(wù)
  • 幻讀
    一個事務(wù)按相同的查詢條件重新讀取以前檢索過的數(shù)據(jù),卻發(fā)現(xiàn)其它事務(wù)插入了滿足其查詢條件的新數(shù)據(jù)。
    時間 事務(wù)A 事務(wù)B
    T1 開啟事務(wù) 開啟事務(wù)
    T2 select * from user where age=18 假設(shè)得到兩條記錄
    T3 向user表插入一條age=18的新記錄
    T4 提交事務(wù)
    T5 再次查詢得到三條記錄
    T6 ..
    T7 提交事務(wù)

幻讀和不可重復(fù)讀的區(qū)別

  • 不可重復(fù)讀的重點是修改:在同一事務(wù)中,相同的條件,第一次和第二次讀到的數(shù)據(jù)不一致(中間有其它事務(wù)提交了修改)。
  • 幻讀的重點是新增或者刪除:在同一事務(wù)中,相同的條件,第一次和第二次讀到的記錄數(shù)不一樣(中間有其它事務(wù)提交了新增或者刪除)。

事務(wù)隔離級別

SQL標(biāo)準(zhǔn)定義了4類隔離級別,每一種級別都規(guī)定了一個事務(wù)中所做的修改,哪些在事務(wù)內(nèi)和事務(wù)間是可見的,哪些是不可見的。

  • Read Uncommited
    所有事務(wù)都可以看到其它未提交事務(wù)的執(zhí)行結(jié)果,該隔離級別一般不會使用。

  • Read Committed(RC)
    一個事務(wù)只能看到已經(jīng)提交的事務(wù)所做的變更。

  • Repeatable Read(RR)
    確保同一事務(wù)的多個實例在并發(fā)讀取數(shù)據(jù)時會看到相同的數(shù)據(jù)行。

  • Serializable

    完全串行化讀,每次讀都需要獲得表級共享鎖,讀寫相互阻塞。

    隔離級別 臟讀 不可重復(fù)讀 幻讀
    Read Uncommited Yes Yes Yes
    Read Committed No Yes Yes
    Repeatable Read No No Yes
    Serializable No No No

    并發(fā)事務(wù)解決方案

    臟讀、不可重復(fù)讀和幻讀都是數(shù)據(jù)庫讀一致性問題,需要由數(shù)據(jù)庫提供一定的事務(wù)隔離機制來解決。

    (1)鎖機制

    解決寫-寫沖突問題。在讀取數(shù)據(jù)前,對其加鎖,防止其它事務(wù)對該數(shù)據(jù)進行修改。

    • 悲觀鎖

      往往依靠數(shù)據(jù)庫提供的鎖機制。

    • 樂觀鎖

      大多是基于數(shù)據(jù)版本記錄機制來實現(xiàn)。

    (2)MVCC多版本并發(fā)控制

    解決讀-寫沖突問題。不用加鎖,通過一定機制生成一個數(shù)據(jù)請求時間點時的一致性數(shù)據(jù)快照, 并用這個快照來提供一定級別 (語句級或事務(wù)級) 的一致性讀取。這樣在讀操作的時候不需要阻塞寫操作,寫操作時不需要阻塞讀操作。

    MVCC多版本并發(fā)控制

    Mysql的大多數(shù)事務(wù)型存儲引擎實現(xiàn)都不是簡單的行級鎖,基于并發(fā)性能考慮,一般都實現(xiàn)了MVCC多版本并發(fā)控制。MVCC是通過保存數(shù)據(jù)在某個時間點的快照來實現(xiàn)的。不管事務(wù)執(zhí)行多長時間,事務(wù)看到的數(shù)據(jù)都是一致的。

    讀操作

    讀操作分成兩類:快照讀和當(dāng)前讀。

    快照讀:簡單的select操作屬于快照讀,不加鎖。

    • select * from table where ? ;

    當(dāng)前讀:特殊的讀操作,插入/更新/刪除操作,屬于當(dāng)前讀,需要加鎖。

    • select * from table where ? lock in share mode ;

    • select * from table where ? for update ;

    • update table set ? where ? ;

    • delete from table where ? ;

    數(shù)據(jù)存儲

    innodb存儲引擎中,每行數(shù)據(jù)都包含了一些隱藏字段:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR和DELETE_BIT。

數(shù)據(jù)庫記錄.png
  • DB_TRX_ID:用來標(biāo)識最近一次對本行記錄做修改的事務(wù)的標(biāo)識符,即最后一次修改本行記錄的事務(wù)id。delete操作在內(nèi)部來看是一次update操作,更新行中的刪除標(biāo)識位DELELE_BIT。
  • DB_ROLL_PTR:指向當(dāng)前數(shù)據(jù)的undo log記錄,回滾數(shù)據(jù)通過這個指針來尋找記錄被更新之前的內(nèi)容信息。
  • DB_ROW_ID:包含一個隨著新行插入而單調(diào)遞增的行ID, 當(dāng)由innodb自動產(chǎn)生聚集索引時,聚集索引會包括這個行ID的值,否則這個行ID不會出現(xiàn)在任何索引中。
  • DELELE_BIT:用于標(biāo)識該記錄是否被刪除。

數(shù)據(jù)操作

  • insert
    創(chuàng)建一條記錄,DB_TRX_ID為當(dāng)前事務(wù)ID,DB_ROLL_PTR為NULL。
  • delete
    將當(dāng)前行的DB_TRX_ID設(shè)置為當(dāng)前事務(wù)ID,DELELE_BIT設(shè)置為1。
  • update 復(fù)制一行,新行的DB_TRX_ID為當(dāng)前事務(wù)ID,DB_ROLL_PTR指向上個版本的記錄,事務(wù)提交后DB_ROLL_PTR設(shè)置為NULL。
  • select
    1、只查找創(chuàng)建早于當(dāng)前事務(wù)ID的記錄,確保當(dāng)前事務(wù)讀取到的行都是事務(wù)之前就已經(jīng)存在的,或者是由當(dāng)前事務(wù)創(chuàng)建或修改的;
    2、行的DELETE BIT為1時,查找刪除晚于當(dāng)前事務(wù)ID的記錄,確保當(dāng)前事務(wù)開始之前,行沒有被刪除。

一致性讀

Mysql的一致性讀是通過read view結(jié)構(gòu)來實現(xiàn)。
read view主要是用來做可見性判斷的,它維護的是本事務(wù)不可見的當(dāng)前其他活躍事務(wù)。其中最早的事務(wù)ID為up_limit_id,最遲的事務(wù)ID為low_limit_id。

    trx_id_t    low_limit_id;
                /*!< The read should not see any transaction
                with trx id >= this value. In other words,
                this is the "high water mark". */
    trx_id_t    up_limit_id;
                /*!< The read should see all trx ids which
                are strictly smaller (<) than this value.
                In other words,
                this is the "low water mark". */

如何理解low_limit_id

可以參考知乎這個答案來理解。low_limit_id應(yīng)該是當(dāng)前系統(tǒng)尚未分配的下一個事務(wù)ID,也就是目前已經(jīng)出現(xiàn)過的事務(wù)ID的最大值+1。

image.png

可見性判斷

假設(shè)要讀取的行的最后提交事務(wù)id(即當(dāng)前數(shù)據(jù)行的穩(wěn)定事務(wù)id)為 trx_id,可見性比較過程如下:

  1. trx_id < up_limit_id => 此記錄的最后一次修改在read view創(chuàng)建之前,跳轉(zhuǎn)到步驟5;
  2. trx_id > low_limit_id => 此記錄的最后一次修改在read view創(chuàng)建之后,跳轉(zhuǎn)到步驟4;
  3. up_limit_id <= trx_id <= low_limit_id => 從up_limit_id到low_limit_id進行遍歷,如果trx_id等于他們之中的某個事務(wù)id的話,表示該記錄的最后一次修改尚未保存,跳轉(zhuǎn)到步驟4。否則跳轉(zhuǎn)到步驟5;
  4. 從此記錄的DB_ROLL_PTR指針?biāo)赶虻膗ndo log(此記錄的上一次修改),將undo log的DB_TRX_ID賦值給trx_id,跳轉(zhuǎn)到步驟1重新開始計算可見性;
  5. 如果此記錄的DELELE_BIT為false,說明該記錄未被刪除,可以返回,否則不返回。

RR和RC隔離級別

Repeatable Read和Read Committed隔離級別都是基于read view來實現(xiàn),不同之處在于:

  • Repeatable Read
    read view是在執(zhí)行事務(wù)中第一條select語句的瞬間創(chuàng)建,后續(xù)所有的select都是復(fù)用這個對象,所以能保證每次讀取的一致性。(可重復(fù)讀的語義
  • Read Committed
    事務(wù)中每條select語句都會創(chuàng)建read view,這樣就可以讀取到其它事務(wù)已經(jīng)提交的內(nèi)容。

對于InnoDB來說,Repeatable Read雖然比Read Committed隔離級別高,開銷反而相對較小。

參考資料

數(shù)據(jù)庫事務(wù)與MySQL事務(wù)總結(jié)
MySQL-InnoDB-MVCC多版本并發(fā)控制
MySQL InnoDB MVCC深度分析
樂觀鎖和 MVCC 的區(qū)別?
MySQL 在 RC 隔離級別下是如何實現(xiàn)讀不阻塞的?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容