MySQL併發控制下的|事務|MVCC|鎖機制|解讀

在這裏插入圖片描述

一、前言

隨着業務發展,對數據庫的併發性能要求也越來越高,不僅要做到高併發還需要在保障數據安全,那麼今天我們聊一聊 MySQL 在高併發下事務、MVCC、鎖機制是如何在高併發情況下維護數據的安全。

二、事務 ACID

  • 爲什麼需要事務:事務是爲了保障用戶的數據操作對數據是安全的。比如我們的銀行卡餘額,我們希望對它的操作是穩定準確的,而且絕對安全
  • ACID:事務的四大特性原子性一致性隔離性持久性
  • 原子性:原子性是指一個事務要麼全部執行,要麼完全不執行。主要是由 innodb 引擎中的 undo 回滾日誌來維護
  • 隔離性:事務在操作過程中不會受到其它事務操作的影響。主要由事務的隔離級別鎖機制共同維護。
  • 持久性:事務操作的結果是具體持久性的,通俗來講就是提交事務後會持久化存儲(落盤)。主要是由 redo log 來維護。
  • 一致性:事務在開始和結束時,數據始終保持一致。由原子性、隔離性、持久性共同維護。

三、多版本併發控制 MVCC

  • 介紹:數據庫的核心方向就是高併發,MySQL 通過併發控制技術維護高併發的環境下數據的一致性和數據安全。MySQL 併發控制有兩種技術方案鎖機制(Locking)多版本併發控制 (MVCC)

  • 鎖機制:通過鎖機制可以維護數據的一致性,但是整體業務場景大多是讀-讀讀-寫寫-寫,三類併發場景,看似容易融合到業務場景後也比較複雜。通過鎖機制主要可以幫助我們解決寫-寫讀-讀 場景下的併發安全問題 則 MVCC 主要幫助解決 讀-寫 問題。

  • MVCC:多版本併發控制,側重優化讀-寫業務的高併發環境。可以解決寫操作時堵塞讀操作併發問題。

  • 一致性非鎖定讀:指 innodb 引擎通過多版本併發控制的方式來讀取,當前執行時間數據庫中的行數據。讀取正在進行 update 或 delete 操作的行,不會等待鎖釋放,而是會讀取該行的快照數據。快照就是指該行之前的版本數據,主要靠 undo 日誌來實現,而 undo 是用於數據庫回滾,因此快照讀本身是沒有開銷的。後臺 Purge 線程也會自動清理一些不需要的 undo 數據。

  • MVCC 兩類讀操作:分爲兩類讀情況 快照讀(Snapshot Read)當前讀(Current Read) 快照讀是讀取數據的可見版本而當前讀則是讀取當前數據的最新版本需要加鎖從而保障其它事務不會修改當前數據。

  • MVCC 實現策略:我們在設計表過程中通過不會直接刪除數據而是設定一個字段來標記,從實現邏輯意義上的刪除。MVCC 的實現方式也與此類似。這種數據管理方式叫數據生命週期管理,其中兩個指標就是標記數據的變化標記數據可用狀態

  • MVCC 下的 DML過程演示:

    Insert進行 insert 操作,事務 id 假設爲 1

    id name create version delete version
    1 test 1

    UpdateMVCC 會先將當前記錄標記爲已刪除在 delete version 字段下設置版本號(原來爲空),然後新增一行數據,寫入相應的版本號,此時爲新版本號爲 2 和上一條數據的 delete version 一致,比如將 name 修改爲 fantasy,如下表:

    id name create version delete version
    1 test 1 2
    1 fantasy 2

    Delete直接將當前數據的 delete version 打上版本號標記爲刪除

    id name create version delete version
    1 test 1 2
    1 fantasy 2 3
  • MVCC 解析:剛纔只是在邏輯層面上介紹 MVCC 的運作方式 create version 和 delete version 維護的是數據的版本信息和數據可用狀態,而實際上還有一個字段是用戶 undo回滾的指針,接下來我們介紹源碼中 MVCC 的實現方式。默認會給每張表加入三個隱藏字段(內部屬性)

    DB_TRX_ID:佔 6 個字節,記錄每一行最近一次修改它的事務 ID
    DB_ROLL_OIR:佔 7 個字節,記錄指向回滾段 undo 日誌的指針
    DB_ROW_ID:佔 6 個字節,當寫入數據時,自動維護自增列

    將三個字段結合就可以標記數據的週期性和,並定位到對應的事務。這就引出 innodb 中實現 MVCC 兩個重要模塊 undo 日誌用來存儲數據的變化 Read View 用來做可見性判斷的, 裏面保存了對本事務不可見的其他活躍事務

  • 注意:對於 innodb 來講,無論是更新還是刪除,都只是設置行記錄上的 deldte BIT 來標記,而並不是真正的刪除記錄,後續這些記錄的清理就需要 Purge 線程來做。還需要注意的是 MVCC 只能在 RCRR 隔離級別下使用,RU讀未提交狀態,所以不存在版本問題,而串行化則會對讀取的數據行加鎖。

四、隔離級別

  • 爲什麼需要隔離級別?
    事務之間如果不互相隔離,那麼就會出現髒讀不可重複讀幻讀

  • 簡單概括髒讀、不可重複讀和幻讀:
    在前,在後:髒讀;
    在前,在後:不可重複讀;
    在前,在後,再:幻讀。

  • 隔離級別與併發問題的關係如下:

    級別 髒讀 不可重複讀 幻讀
    讀未提交(READ-UNCOMMITTED)
    不可重複讀(READ-COMMITTED)
    可重複讀(REPEATABLE-READ) 🆚
    串行化(SERIALIZABLE)

    其中串行化隔離級別雖然解決了所有數據問題,但是卻帶來了併發的性能問題,而讀未提交的隔離級別違反了基礎事務的安全處理要求,所以我們在選擇隔離級別時都會在 RC 和 RR 中選擇,MySQL 默認隔離級別爲 RR 級別。

    -- 查詢數據庫中的隔離級別
    select @@transaction_isolation;
    -- 臨時設置MySQL數據庫中的隔離級別
    set global transaction_isolation='READ-COMMITTED';
    
  • RC 和 RR 的區別:RC 在事務可以讀取到其它事務提交的事務數據,而對於 RR 級別來講,它會保證在一個事務中數據多次的查詢結果是不變的,儘管其它事務已經提交了改動。從鎖的角度來講 RC 的性能會優於 RR。

  • RC 和 RR 級別下的快照讀:RC 級別下的快照讀總是會讀取被鎖定行的最新版本的一份快照數據,而 RR 級別下的快照讀總是會讀取事務開始時行版本的數據。這個怎麼理解呢?請看如下案例:


    首先打開 MySQL 會話 Session 1 開啓一個事務然後查詢一條數據


    在這裏插入圖片描述


    開啓另一個 MySQL 會話 Session 2 模擬併發場景,開啓事務修改 id1 = 3 中的 id2 爲 8023


    在這裏插入圖片描述


    Session 2 中我們修改了 id2 20170831 爲 8023 但是還未提交,此時 id1 = 3 的行已經加上了一個 X 排它鎖,此時再讀取 Session 1 會話中的 id1 = 3 的記錄根據 innodb 引擎的特性,即在 RR 和 RC 事務隔離級別下會使用 “非鎖定一致性讀” 也就是快照讀。接者 Session 1 未提交的事務再此運行查詢 id1 = 3 的 SQL 語句,此時無論此時隔離級別是 RR 和 RC 結果都如下圖:


    在這裏插入圖片描述


    接者我們提交 Session 2 中的事務。


    在這裏插入圖片描述


    Session 2 中的事務提交後,在 Session 1 中再次執行查詢 id1 = 3 的 SQL 語句,此時在 RRRC 級別下運行的結果就不同了,RC 事務的隔離級別,總是讀取最新版本快照數據,因爲 Session2 提交了事務更新快照版本,所以 Session 1 在事務中可以讀取 Session 2 中已經提交的改動,結果如下:


    在這裏插入圖片描述


    因爲 RR 級別讀取數據快照總是讀取開始事務前的行版本的快照數據,所以儘管 Session 2 更新了快照版本,RR 級別下事務未提交之前不會受到影響,所以 RR 級別下兩次讀取數據的結果都相同


    在這裏插入圖片描述

五、鎖機制

  • 什麼是鎖?
    鎖是計算機協調多個進程或線程併發訪問某一資源的機制。

  • innodb 兩種鎖:
    共享鎖 S:允許一個事務去一行,阻止其它事務獲得相同數據集的排它鎖。通俗來講就是可以重複讀,沒讀完時不允許寫。
    排它鎖 X:允許獲得排它鎖的事務更新數據,阻止其它事務獲得相同數據集的排他鎖共享鎖。通俗來講就是寫的時候不允許其它事務寫和讀。

  • 發現問題:有兩個事務 A 和 B,事務 A 鎖住了表中的一行數據,加上行鎖 S,即這一行只能讀不能寫。之後事務 B 又申請整張的寫鎖 (mysql 中可以使用 lock table xxx write 鎖表),正常邏輯來將,事務 B 就可以修改表中任意行數據,包括事務 A 鎖住的那一行數據,實際情況則會發生鎖衝突,現在就需要一種機制來判斷是否有行鎖,比如鎖表前先判斷每一行數據是否有行鎖,但是這種方案在隨着數據量增大代價會無限放大,肯定是不取的,而意向鎖就是來解決衝突的協調者

  • 意向鎖工作流程:
    事務 A 首先需要申請表的意向鎖,成功後申請一行的行鎖。
    事務 B 申請排它鎖,但是發現表中已經有意向共享鎖,說明表中的某行數據已經被鎖定,此時申請的寫鎖會被堵塞

  • 意向共享鎖 IS:事務給某行數據加入共享鎖前,需要先申請意向共享鎖。通俗來講,就是一個數據行加共享鎖前必須要先取得該表的意向共享鎖。

  • 意向排它鎖 IX:與上類似,加入排它鎖前需要先獲得意向排它鎖

  • innodb 行鎖:行鎖是通過給索引加鎖來實現的,不用擔心表中是否創建了索引,如果有主鍵 MySQL 會在主鍵上創建聚簇索引用於回表查詢,如果沒有主鍵則考慮 unique 約束的字段,如果前面兩種都不滿足,則會創建隱藏列 RowID 作爲聚簇索引。如果不通過索引檢索數據,那麼 innodb 引擎會對錶中所有的數據加鎖,實際效果與表鎖相同,所以要儘可能讓所有數據都通過索引來完成,避免行鎖升級爲表鎖。

    innodb 下的三種行鎖:
    行鎖(Record Lock):對索引加鎖,即鎖定一行記錄。
    間隙鎖(Gap Lock):對索引項之間的間隙、對第一條記錄前的間隙或最後一條記錄後的間隙加鎖,即鎖定一個範圍的記錄,不包含記錄本身。
    Next-Key Lock:鎖定一個範圍的記錄幷包含記錄本身。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章