阿里面試:事務ACID,底層是如何實現的?

文章很長,且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 博客園版 爲您奉上珍貴的學習資源 :

免費贈送 :《尼恩Java面試寶典》 持續更新+ 史上最全 + 面試必備 2000頁+ 面試必備 + 大廠必備 +漲薪必備
免費贈送 :《尼恩技術聖經+高併發系列PDF》 ,幫你 實現技術自由,完成職業升級, 薪酬猛漲!加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷1)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷2)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷3)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領

免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取


阿里面試:事務ACID,底層是如何實現的?

尼恩特別說明: 尼恩的文章,都會在 《技術自由圈》 公號 發佈, 並且維護最新版本。 如果發現圖片 不可見, 請去 《技術自由圈》 公號 查找

尼恩說在前面

在40歲老架構師 尼恩的讀者交流羣(50+)中,很多小夥伴拿到了一線互聯網企業如得物、阿里、滴滴、極兔、有贊、希音、百度、網易、美團的面試機會,遇到很多很重要的面試題:

事務ACID,底層是如何實現的?

ACID靠什麼保證的?

ACID的一致性是指什麼?是如何實現的?

ACID中的持久性是指什麼?是如何實現的?

ACID中的隔離性是指什麼?是如何實現的?

最近有小夥伴在面試阿里,又遇到了MVCC相關的面試題。小夥伴 支支吾吾的說了幾句,沒說清楚,面試掛了。

所以,尼恩給大家做一下系統化、體系化的梳理,使得大家內力猛增,可以充分展示一下大家雄厚的 “技術肌肉”,讓面試官愛到 “不能自已、口水直流”,然後實現”offer直提”。

這裏,尼恩團隊把MVCC/ 事務 進行了全面的梳理,穿透式的梳理,

梳理爲一個PDF文檔 《MVCC 學習聖經:一次穿透MYSQL MVCC 》, 並且持續迭代。

這個文檔將成爲大家 面試的殺手鐗, 此文當最新PDF版本,可以找40歲老架構師尼恩獲取。

當然,上面的面試題以及參考答案,也會收入咱們的 《尼恩Java面試寶典PDF》V171版本,供後面的小夥伴參考,提升大家的 3高 架構、設計、開發水平。

最新《尼恩 架構筆記》《尼恩高併發三部曲》《尼恩Java面試寶典》的PDF,請關注本公衆號【技術自由圈】獲取,回覆:領電子書

本文作者:

  • 第一作者 Moen (負責寫初稿 )
  • 第二作者 尼恩 (40歲老架構師, 負責提升此文的 技術高度,讓大家有一種 俯視 技術的感覺)

什麼是事務

事務(Transaction)是數據庫管理系統執行過程中的一個邏輯單位,它由一個有限的數據庫操作序列構成。

事務(Transaction)是訪問和更新數據庫的程序執行單元;

事務中可能包含一個或多個sql語句,這些語句要麼都執行,要麼都不執行。

這些操作要麼全部執行,要麼全部不執行,是一個不可分割的工作單位。

事務的目的是確保數據的完整性和一致性,它通過一系列的操作,將數據庫從一個一致性狀態轉換到另一個一致性狀態。

首先回顧一下MySQL事務的基礎知識。

MySQL邏輯架構和存儲引擎

img

如上圖所示,MySQL服務器邏輯架構從上往下可以分爲三層:

(1)第一層:處理客戶端連接、授權認證等。

(2)第二層:服務器層,負責查詢語句的解析、優化、緩存以及內置函數的實現、存儲過程等。

(3)第三層:存儲引擎,負責MySQL中數據的存儲和提取。

MySQL 中服務器層不管理事務,事務是由存儲引擎實現的。

MySQL支持事務的存儲引擎有InnoDB、NDB Cluster等,其中InnoDB的使用最爲廣泛;其他存儲引擎不支持事務,如MyIsam、Memory等。

如無特殊說明,後文中描述的內容都是基於InnoDB。

innodb 一個數據庫事務( transaction)的執行過程

數據庫事務( transaction) 是指作爲一個邏輯工作單元執行的一系列數據庫操作,這些操作要麼全部成功,要麼全部失敗,是一個不可分割的工作單元。

事務是在事務開始和事務結束之間執行的一系列數據庫操作。

事務的目的是確保數據庫操作的一致性和完整性,同時提供對併發訪問的隔離性和恢復能力。

圖解:一個簡化版的執行過程

下面是一個簡化版的執行過程

圖片

注意:請點擊圖像以查看清晰的視圖!

圖解:一個詳細版的一個數據庫事務( transaction)的執行過程

下面是一個詳細版的一個數據庫事務( transaction)的執行過程

圖片

注意:請點擊圖像以查看清晰的視圖!

InnoDB的一次更新事務涉及到多個組件和步驟,包括Buffer Pool、BinLog、UndoLog、RedoLog以及物理磁盤。

下面是一次完整的事務更新操作過程:

step1. 開始數據,InnoDB 收到更新請求

執行SQL,收到事務的更新請求

step2. 加載數據到緩存中(Buffer Pool)

在進行數據更新時,InnoDB首先在緩衝池(Buffer Pool)中查找待更新記錄是否已經在內存中。若記錄不在內存中,InnoDB會將記錄從磁盤文件讀取到緩衝池(Buffer Pool)中。

緩衝池是InnoDB存儲引擎提供的臨時存儲區,用於提升數據讀取和修改的速度。將數據加載到緩衝池後,後續的更新操作均在緩衝池內進行。這樣可以減少磁盤I/O操作,從而提高事務處理速度。緩衝池在內存中存儲數據,可以降低磁盤I/O的開銷,提高數據讀取和寫入的速度,從而優化事務處理性能。

step3. 寫入Undo Log

在更新數據之前,InnoDB會將原始數據的副本寫入Undo Log(回滾日誌)。

Undo Log是保證事務回滾和併發控制的關鍵部分,也是確保事務原子性和一致性的重要機制。Undo Log記錄了事務開始前的數據狀態,以便在需要回滾時進行數據恢復。通過記錄撤銷日誌,InnoDB能夠實現事務的滾動回滾,提高事務處理的靈活性。撤銷日誌在事務處理過程中起到了關鍵作用,它記錄了事務的修改過程,使得事務能夠在需要時回滾到之前的狀態,保證數據的一致性和完整性。

step4. 更新內存數據,包括 Buffer Pool和 Redo Log Buffer

接下來,InnoDB會在緩衝池中更新數據。

這意味着,當執行update語句時,InnoDB會先更新已經讀取到Buffer Pool中的數據,修改操作會直接在內存中進行,而不是立即寫入磁盤。

此時,緩衝池中的數據被標記爲"髒頁",表示與磁盤上的數據不一致。髒頁是緩衝池中已經被修改但尚未寫入磁盤的數據頁,它需要後續的處理才能將修改同步到磁盤,保證數據的持久性。

更新Buffer Pool和Redo Log Buffer通常是數據庫系統內部的操作,這些操作可以作爲事務處理的一部分。

更新Buffer Pool和Redo Log Buffer的一般步驟:

第一步:Buffer Pool更新:
緩衝池 (Buffer Pool) 是數據庫系統中用於存儲數據頁的內存區域。當需要讀取或寫入數據時,數據庫系統首先會檢查緩衝池,如果數據頁已經在緩衝池中,就可以直接進行訪問,而不必去訪問磁盤。
要更新Buffer Pool,首先需要確定要讀取或寫入的數據頁。如果數據頁已經在緩衝池中,可以直接進行讀取或寫入操作;如果數據頁不在緩衝池中,則需要將其從磁盤加載到緩衝池中。
更新Buffer Pool的過程通常是由數據庫系統自動管理的,但也可以通過合適的API或查詢語句手動進行。

第二步:Redo Log Buffer更新:
重做日誌(Redo Log)是數據庫系統用於持久化事務操作的一種技術。

當事務進行數據更新時,數據庫系統會首先將更新操作記錄到Redo Log中,以確保即使在系統崩潰時也能夠恢復到事務之前的狀態。
Redo Log Buffer是一個內存區域,用於暫時存儲事務更新操作的日誌記錄。在事務進行數據更新時,數據庫系統會將更新操作記錄到Redo Log Buffer中,然後定期將其寫入到磁盤的Redo Log文件中。
更新Redo Log Buffer通常是在事務進行提交(Commit)時完成的。

當事務提交時,數據庫系統會確保將Redo Log Buffer中的所有日誌記錄寫入到磁盤中,以保證事務的持久性。

在更新Buffer Pool和Redo Log Buffer時,數據庫系統通常會採取一些優化措施,例如異步寫入、批量提交等,以提高性能和減少IO開銷。同時,數據庫管理員也可以通過調整參數和配置來優化Buffer Pool和Redo Log Buffer的性能。

step5 寫入Redo Log 文件

爲了保證事務的持久性,InnoDB在Buffer Pool中記錄修改操作的同時,InnoDB會先將更新操作寫入Redo Log(重做日誌)。

Redo Log是一種物理日誌,它記錄了事務對數據庫的修改操作。

通過Redo Log,即使系統發生故障,也可以通過重做日誌來恢復事務修改後的狀態。

這一機制保證了事務的可靠性,降低了系統故障帶來的風險。

重做日誌是保證數據持久性和恢復性的關鍵,它記錄了事務的修改過程,使得事務的修改能夠在故障恢復後得到恢復。

step6. 提交事務

當事務完成所有的更新操作後,事務被提交。

在提交事務時,InnoDB會將事務標記爲"準備提交"狀態。

此時,事務的修改操作仍然在緩衝池中,尚未寫入磁盤。

事務提交是事務處理的重要環節,它標誌着事務處理完畢,可以進行後續的提交操作。

在提交之前,事務的修改操作需要得到處理,保證數據的完整性和一致性。

step7. 寫入BinLog

在事務提交之後,InnoDB會將事務的修改操作寫入BinLog(歸檔日誌)。

BinLog是MySQL的二進制日誌,用於記錄數據庫的所有修改操作。

在歸檔日誌中記錄的信息包括:事務開始的時間、數據庫名、表名、事務ID、SQL語句等。

它可以用於數據恢復、主從複製、數據分析和同步等場景。

歸檔日誌在數據庫中起到了關鍵作用,它記錄了數據庫的修改過程,使得數據庫的修改能夠在故障恢復後得到恢復。

step8. 刷新髒頁到磁盤

最後,在提交過程完成後,InnoDB會將緩衝池(Buffer Pool)中的髒頁刷新到物理磁盤上的數據文件中。

這個過程稱爲"刷髒"。

通過刷髒操作,將緩衝池中的修改操作同步到磁盤,確保數據的持久性。

然而,這個寫入過程並非立即執行,而是由後臺線程異步執行的,因此可能會有一定的延遲。

總而言之,MySQL會在適當的時機選擇將數據寫入磁盤以進行持久化。

典型的MySQL事務

典型的 MySQL 事務通常包括以下步驟:

  1. 開始事務:使用 START TRANSACTIONBEGIN 命令來開始一個新的事務。這將確保接下來的操作將被視爲一個事務單元,並且要麼全部成功提交,要麼全部失敗回滾。
  2. 執行SQL操作:在事務中執行各種SQL操作,例如插入、更新或刪除數據,查詢等。這些操作可能會涉及一個或多個數據庫表。
  3. 數據處理與驗證:在執行SQL操作之前或之後,進行數據處理和驗證。這可能包括檢查約束條件、驗證輸入數據的有效性等。
  4. 提交或回滾事務:如果所有的數據庫操作都成功,並且通過了數據驗證步驟,那麼事務可以被提交(COMMIT)。這將導致所有的更改永久地應用到數據庫中。如果在任何時候出現了錯誤或者違反了事務的約束條件,那麼整個事務將會被回滾(ROLLBACK),所有的更新將被撤銷,數據庫將恢復到事務開始之前的狀態。
  5. 結束事務:一旦事務被提交或者回滾,事務就結束了。可以使用 COMMITROLLBACK 命令來結束事務。此時數據庫會釋放任何由事務佔用的資源。

以下是一個典型的MySQL事務的示例:

START TRANSACTION;

-- 執行SQL操作
INSERT INTO orders (customer_id, total_amount) VALUES (123, 100);
UPDATE customers SET points = points + 10 WHERE id = 123;

-- 數據處理與驗證
IF (SELECT total_amount FROM orders WHERE id = LAST_INSERT_ID()) > 50 THEN
    COMMIT;
ELSE
    ROLLBACK;
END IF;

-- 提交或回滾事務
COMMIT;

在這個示例中,事務首先通過 START TRANSACTION 開始,然後執行一系列SQL操作,包括向 orders 表中插入一條新的訂單記錄和更新 customers 表中客戶的積分。接着進行了數據處理和驗證,如果訂單的總金額大於50,則提交事務,否則回滾事務。最後使用 COMMIT 命令提交事務。

典型的MySQL事務是如下操作的:

START TRANSACTION;

-- 執行SQL操作

-- 提交
COMMIT;

其中start transaction標識事務開始,commit提交事務,將執行結果寫入到數據庫。

如果sql語句執行出現問題,會調用rollback,回滾所有已經執行成功的sql語句。

START TRANSACTION;

-- 執行SQL操作

-- 回滾
ROLLBACK;

當然,也可以在事務中直接使用rollback語句進行回滾。

MySQL自動提交

在 MySQL 中,默認情況下是開啓了自動提交(Auto-Commit)模式的。如下所示:

img

在自動提交模式下,如果沒有 start transaction 顯式地開始一個事務,那麼每個sql語句都會被當做一個事務執行提交操作。這意味着每個單獨的 SQL 語句都會被作爲一個事務並立即提交。

這種模式的好處是簡化了對數據庫的操作,但也可能會導致性能問題或數據不一致。

通過如下方式,可以關閉autocommit;需要注意的是,autocommit參數是針對連接的,在一個連接中修改了參數,不會對其他連接產生影響。

要在 MySQL 中禁用自動提交,可以使用以下 SQL 語句:

SET autocommit = 0;

這樣設置之後,直到顯式執行了 COMMIT 或 ROLLBACK 語句,MySQL 將不會自動提交事務。

在事務執行完成後,你可以使用 COMMIT 來提交事務並將更改保存到數據庫中,或者使用 ROLLBACK 來撤銷事務中的所有更改。

特殊操作

在MySQL中,存在一些特殊的命令,如果在事務中執行了這些命令,會馬上強制執行commit提交事務;

如DDL語句(create table/drop table/alter/table)、lock tables語句等等。

不過,常用的select、insert、update和delete命令,都不會強制提交事務。

事務的ACID特性

ACID,是指數據庫管理系統(DBMS)在寫入或更新資料的過程中,爲保證事務(transaction)是正確可靠的,

ACID是衡量事務的四個特性:

  • 原子性(Atomicity,或稱不可分割性)
  • 一致性(Consistency)
  • 隔離性(Isolation)
  • 持久性(Durability)

事務通常具有以下四個特性,也被稱爲ACID屬性:

  1. 原子性(Atomicity):事務作爲一個整體執行,包含在其中的對數據庫的操作要麼全部執行,要麼全部不執行。

  2. 一致性(Consistency):事務必須使數據庫從一個一致性狀態變換到另一個一致性狀態。也就是說,一個事務的執行不能破壞數據庫數據的完整性和一致性。

  3. 隔離性(Isolation):事務的執行不受其他事務的干擾,事務執行的中間結果對其他事務是不可見的。

  4. 持久性(Durability):一旦事務提交,則其結果就是永久性的,即使系統崩潰也不會丟失。

    事務的這些特性確保了即使在高併發的環境中,數據庫也能保持數據的完整性和一致性。在數據庫系統中,事務是通過一系列的操作來完成的,包括數據的插入、更新、刪除等。如果事務中的任何操作失敗,或者因爲某種原因被中斷,那麼整個事務都會回滾(Rollback),即撤銷所有已經執行的操作,使數據庫回到事務開始之前的狀態。如果事務中的所有操作都成功完成,那麼事務會提交(Commit),所做的更改會永久保存到數據庫中。

4種事務隔離級別

什麼是事務個隔離級別?

事務隔離級別主要定義了事務在併發執行時的行爲,特別是它們如何與其他事務交互以及它們如何看到數據庫中的更改。

ANSI/ISO SQL標準定義了4中事務隔離級別:未提交讀(read uncommitted),提交讀(read committed),重複讀(repeatable read),串行讀(serializable)。

  • Oracle中默認的事務隔離級別是提交讀 (read committed)。
  • 對於MySQL的Innodb的默認事務隔離級別是重複讀(repeated read)。

MySQL支持四種不同的事務隔離級別,每種級別都有其特定的行爲和適用場景。以下是MySQL的四種事務隔離級別及其描述:

  1. READ UNCOMMITTED(讀取未提交)

    • 允許讀取尚未提交的數據變更。
    • 這是最低的隔離級別,它可能導致髒讀、不可重複讀和幻讀。
    • 在這個級別,一個事務可以讀取到另一個尚未提交事務的修改,這可能導致數據的不一致性。
  2. READ COMMITTED(讀取已提交)

    • 只允許讀取併發事務已經提交的數據。
    • 這個級別可以防止髒讀,但仍可能導致不可重複讀和幻讀。
    • 在這個級別,每個事務只能看到它開始時的數據狀態以及它提交時其他事務所做的提交。
  3. REPEATABLE READ(可重複讀取)

    • 這是MySQL的默認隔離級別
    • 它確保在同一事務中多次讀取同一數據時,看到的是相同的數據版本,即使其他事務在此期間修改了這些數據。
    • 儘管可以避免髒讀和不可重複讀,但在這個級別下仍可能出現幻讀(即在一個事務中,兩次相同的查詢可能會返回不同的結果集,因爲其他事務在此期間插入了新的記錄)。
  4. SERIALIZABLE(可串行化)

    • 這是最高的隔離級別。
    • 它通過強制事務串行執行來避免髒讀、不可重複讀和幻讀。
    • 在這個級別,每個事務在執行時都會完全鎖定它所訪問的數據,從而確保數據的一致性。但這也可能導致性能下降,因爲併發事務必須等待其他事務完成才能執行。

    選擇適當的事務隔離級別需要根據應用的需求和性能考慮進行權衡。

    在某些情況下,可能需要更高的隔離級別來確保數據的一致性,而在其他情況下,可能需要降低隔離級別以提高性能。

    同時,也需要注意不同隔離級別可能帶來的併發問題,如髒讀、不可重複讀和幻讀等。

髒讀(Dirty Read)
一個事務讀取到另一個尚未提交事務的修改。

不可重複讀(Non-repeatable Read)
在同一個事務內,多次讀取同一數據返回的結果有所不同。

幻讀(Phantom Read)
一個事務在執行兩次相同的查詢時,因爲另一個併發事務的插入或刪除操作,導致兩次查詢返回的結果集不同。

ACID的 A 原子性如何實現?

原子性定義

原子性是指一個事務是一個不可分割的工作單位,其中的操作要麼都做,要麼都不做;

如果事務中一個sql語句執行失敗,則已執行的語句也必須回滾,數據庫退回到事務前的狀態。

原子性是ACID(原子性、一致性、隔離性和持久性)的四個基本特徵之一,它確保數據庫操作要麼全部成功,要麼全部失敗,不存在部分完成的情況。

原子性的實現機制

原子性的實現主要依賴於以下機制:

  1. 事務:事務是一系列數據庫操作的邏輯單位,它們要麼全部成功執行,要麼全部失敗回滾。事務可以通過SQL語句或編程接口來啓動、提交或回滾。在一個事務內執行的所有操作都視爲一個原子操作。
  2. redo log日誌:數據庫系統使用重做日誌 來記錄事務執行過程中的所有數據庫操作。這些日誌記錄包括在事務開始之前的數據狀態,以及事務執行過程中對數據的任何修改。這些日誌記錄可以用於在系統故障後恢復數據庫到一致狀態。
  3. Undo日誌:除了記錄事務執行的操作之外,數據庫系統還會記錄撤銷操作(undo),即使在事務提交之前也會記錄。這些undo日誌記錄用於在事務回滾時撤銷對數據的修改,以確保原子性。如果事務失敗,系統可以使用undo日誌來還原到事務開始之前的狀態。
  4. 鎖機制:數據庫系統使用鎖來控制併發訪問,確保在一個事務執行過程中,其他事務無法修改被當前事務使用的數據。這樣可以防止其他事務對當前事務的操作造成干擾,從而維護原子性。

通過這些機制的配合,數據庫系統能夠有效地實現原子性。

當事務提交時,系統會將所有的數據庫操作應用到數據庫中,然後將這些操作記錄寫入日誌,以確保這些操作在系統故障後仍然能夠恢復。如果事務失敗或者回滾,系統會使用undo日誌來撤銷事務執行過程中對數據的任何修改,以保證數據庫的一致性和完整性。

以上是原子性保證的比較全面的答案。 但是,一般情況下,大家都認爲,

  • 其中redo log用於保證事務持久性;
  • undo log 則是事務原子性和隔離性實現的基礎。

所以,大部分人都認爲, 原子性實現的核心,是利用Innodb的undo log 重做日誌。

MySQL的七種日誌

錯誤日誌(error log)

error log主要記錄MySQL在啓動、關閉或者運行過程中的錯誤信息,在MySQL的配置文件my.cnf中,

可以通過log-error=/var/log/mysqld.log 執行mysql錯誤日誌的位置。

慢查詢日誌(slow query log)

  • MySQL的慢查詢日誌是MySQL提供的一種日誌記錄,它用來記錄在MySQL中響應時間超過閥值的語句,具體指運行時間超過long_query_time值的SQL,則會被記錄到慢查詢日誌中。

  • 具體指運行時間超過long_query_time值的SQL,則會被記錄到慢查詢日誌中。long_query_time的默認值爲10,意思是運行10秒以上的語句。

  • 由他來查看哪些SQL超出了我們的最大忍耐時間值,比如一條sql執行超過5秒鐘,我們就算慢SQL,希望能收集超過5秒的sql,結合之前explain進行全面分析

  • 在生產環境中,如果要手工分析日誌,查找、分析SQL,顯然是個體力活,MySQL提供了日誌分析工具mysqldumpslow。

一般查詢日誌(general log)

general log 記錄了客戶端連接信息以及執行的SQL語句信息,包括客戶端何時連接了服務器、客戶端發送的所有SQL以及其他事件,比如 MySQL 服務啓動和關閉等等。

重寫日誌(redo log)

redo log 屬於MySQL存儲引擎InnoDB的事務日誌。

回滾日誌(undo log)

undo log屬於邏輯日誌,如其名主要起到回滾的作用,它是保證事務原子性的關鍵。

二進制日誌(bin log)

bin log是一種數據庫Server層(和什麼引擎無關),以二進制形式存儲在磁盤中的邏輯日誌。

具體的七大日誌的介紹, 具體請參考下面的文章

美團一面:聊聊MySQL的七種日誌

七大日誌中,其中redo log用於保證事務持久性;undo log則是事務原子性和隔離性實現的基礎。

原子性的關鍵undo log

undo log名爲回滾日誌,是實現原子性的關鍵,當事務回滾時能夠撤銷所有已經成功執行的sql語句,他需要記錄你要回滾的相應日誌信息。

undo log是實現原子性的關鍵,是當事務回滾時,能夠撤銷所有已經成功執行的sql語句。

InnoDB實現回滾,靠的是undo log:

  • 當事務對數據庫進行修改時,InnoDB會生成對應的undo log;

  • 如果事務執行失敗或調用了rollback,導致事務需要回滾,便可以利用undo log中的信息將數據回滾到修改之前的樣子。

undo log屬於邏輯日誌,它記錄的是sql執行相關的信息。當發生回滾時,InnoDB會根據undo log的內容做與之前相反的工作:

  • (1)當你delete一條數據的時候,就需要記錄這條數據的信息,回滾的時候,insert這條舊數據
  • (2)當你update一條數據的時候,就需要記錄之前的舊值,回滾的時候,根據舊值執行update操作
  • (3)當年insert一條數據的時候,就需要這條記錄的主鍵,回滾的時候,根據主鍵執行delete操作

以update操作爲例:當事務執行update時,其生成的undo log中會包含被修改行的主鍵(以便知道修改了哪些行)、修改了哪些列、這些列在修改前後的值等信息,回滾時便可以使用這些信息將數據還原到update之前的狀態。

undo log記錄了這些回滾需要的信息,當事務執行失敗或調用了rollback,導致事務需要回滾,便可以利用undo log中的信息將數據回滾到修改之前的樣子。

ACID的 D 持久性如何實現?

ACID的持久性定義

ACID的持久性是指一旦事務提交,其所做的更改將永久保存在數據庫中,即使系統發生故障也不會丟失。

ACID的持久性包括兩個方面:

  • 事務一旦提交,它對數據庫的改變就應該是永久性的。

  • 接下來的其他操作或故障不應該對其有任何影響。

持久性的實現機制

持久性的實現主要依賴於以下機制:

  1. 事務日誌(Redo Log):數據庫系統將事務操作記錄到事務日誌中。這些日誌包含了事務執行的所有更新操作,包括對數據頁的修改。在事務提交時,數據庫系統會將事務日誌中的操作應用到數據庫中的數據文件,確保事務的更新被永久保存下來。
  2. 數據文件持久性:數據庫系統確保在將事務操作應用到數據文件之前,先將其記錄到事務日誌中。只有在事務日誌中的操作被成功地寫入到磁盤之後,數據庫系統纔會將這些操作應用到數據文件中。這樣可以確保即使在系統崩潰時,數據庫系統也可以通過重新應用事務日誌來恢復到事務提交之後的狀態。
  3. 寫前日誌(Write-Ahead Logging, WAL):一種常見的持久性實現方法是使用寫前日誌。在執行更新操作之前,數據庫系統首先將更新操作寫入到事務日誌中,然後再將更新操作應用到數據庫中。這樣可以確保在事務提交之前,更新操作已經被記錄到日誌中,即使系統發生故障,也可以通過重新應用日誌來恢復數據。
  4. 數據庫備份:定期對數據庫進行備份也是確保數據持久性的一種重要手段。通過備份,即使發生嚴重的系統故障,也可以通過恢復備份數據來重新構建數據庫,確保數據不會永久丟失。

通過以上機制的配合,數據庫系統可以確保事務提交後的更改是持久的,即使在系統崩潰或故障的情況下,也可以通過事務日誌來恢復數據,從而實現持久性。

一般認爲,實現持久性的核心組件 是 redo log

持久性的實現核心組件:redo log

redo log和undo log都屬於InnoDB的事務日誌。

下面看看一下redo log存在的背景。

InnoDB作爲MySQL的存儲引擎,數據是存放在磁盤中的,但如果每次讀寫數據都需要磁盤IO,效率會很低。

這裏,也採用了緩存的架構, 類似CacheAside的模式。

InnoDB提供了緩存(Buffer Pool),Buffer Pool中包含了磁盤中部分數據頁的映射,作爲訪問數據庫的緩衝:當從數據庫讀取數據時,會首先從Buffer Pool中讀取,如果Buffer Pool中沒有,則從磁盤讀取後放入Buffer Pool;當向數據庫寫入數據時,會首先寫入Buffer Pool,Buffer Pool中修改的數據會定期刷新到磁盤中(這一過程稱爲刷髒)。

圖片

Buffer Pool的使用大大提高了讀寫數據的效率,但是也帶了新的問題:如果MySQL宕機,而此時Buffer Pool中修改的數據還沒有刷新到磁盤,就會導致數據的丟失,事務的持久性無法保證。

於是,redo log被引入來解決這個問題:當數據修改時,除了修改Buffer Pool中的數據,還會在redo log記錄這次操作;當事務提交時,會調用fsync接口對redo log進行刷盤。如果MySQL宕機,重啓時可以讀取redo log中的數據,對數據庫進行恢復。

redo log採用的是WAL(Write-ahead logging,預寫式日誌),所有修改先寫入日誌,再更新到Buffer Pool,保證了數據不會因MySQL宕機而丟失,從而滿足了持久性要求。

在 MySQL 中,Redo Log(重做日誌)實際上是一種實現 WAL(寫前日誌)機制的方式。MySQL 中的 Redo Log 是用於保證事務持久性和崩潰恢復的重要組成部分。

Redo Log 的基本原理如下:

  1. 記錄數據變化:在執行數據更新操作時,MySQL 將這些更新操作記錄到 Redo Log 中。這些記錄包括對數據頁的修改、索引更新等。
  2. 持久化到磁盤:提交事務之前,Redo Log 中的記錄會被 寫入到磁盤上的 Redo Log 文件中。這樣即使在將更新操作應用到數據文件之前,相應的 Redo Log 記錄已經持久化到磁盤上。
  3. 崩潰恢復:如果系統在將更新操作應用到數據文件之前發生崩潰,MySQL 可以通過重新應用 Redo Log 來恢復數據。MySQL 在啓動時會檢查 Redo Log 文件,如果發現未完成的事務,則會嘗試回滾或者重新應用這些事務,以確保數據庫狀態的一致性。

Redo Log 實際上是 WAL 機制的具體實現,在 MySQL 中它用於記錄事務執行過程中的數據更新操作,並確保這些操作的持久性。通過 Redo Log,MySQL 可以在系統崩潰或者故障的情況下,通過重新應用 Redo Log 來恢復數據庫到事務提交之後的狀態,從而保證了數據的一致性和持久性。

提交事務之前,Redo Log 中的記錄會被 寫入到磁盤上的 Redo Log 文件中。

圖片

既然redo log也需要在事務提交時將日誌寫入磁盤,爲什麼它比直接將Buffer Pool中修改的數據寫入磁盤(即刷髒)要快呢?主要有以下兩方面的原因:

(1)刷髒是隨機IO,因爲每次修改的數據位置隨機,但寫redo log是追加操作,屬於順序IO。

(2)刷髒是以數據頁(Page)爲單位的,MySQL默認頁大小是16KB,一個Page上一個小修改都要整頁寫入;而redo log中只包含真正需要寫入的部分,無效IO大大減少。

爲什麼不是binlog 保證持久性?

我們知道,在MySQL中還存在binlog(二進制日誌)也可以記錄寫操作並用於數據的恢復,但二者是有着根本的不同的:

(1)作用不同:redo log是用於crash recovery的,保證MySQL宕機也不會影響持久性;binlog是用於point-in-time recovery的,保證服務器可以基於時間點恢復數據,此外binlog還用於主從複製。

(2)層次不同:redo log是InnoDB存儲引擎實現的,而binlog是MySQL的服務器層(可以參考文章前面對MySQL邏輯架構的介紹)實現的,同時支持InnoDB和其他存儲引擎。

(3)內容不同:redo log是物理日誌,內容基於磁盤的Page;binlog的內容是二進制的,根據binlog_format參數的不同,可能基於sql語句、基於數據本身或者二者的混合。

(4)寫入時機不同:binlog在事務提交時寫入;redo log的寫入時機相對多元:

  • 前面曾提到:當事務提交時會調用fsync對redo log進行刷盤;這是默認情況下的策略,修改innodb_flush_log_at_trx_commit參數可以改變該策略,但事務的持久性將無法保證。
  • 除了事務提交時,還有其他刷盤時機:如master thread每秒刷盤一次redo log等,這樣的好處是不一定要等到commit時刷盤,commit速度大大加快。

ACID的I 隔離性(Isolation)如何實現?

隔離性是指,事務內部的操作與其他事務是隔離的,併發執行的各個事務之間不能互相干擾。

與原子性、持久性等側重於研究事務本身不同,隔離性呢,隔離性研究的是不同事務之間的相互影響,就好比山高水長,事務內部的操作與其他事務彷彿天各一方,絕不互相交融。

圖片

就像古人云:“學而不思則罔,思而不學則殆。”我們要學會借古人智慧,方能把握事務特性的真諦。

在我們的討論中,咱們要分清事務的不同側重點,不可混淆哦。

要是要講嚴謹的話,隔離性可分爲4不同的等級,隔離級別最高的是“可串行化”,但是性能最低。

可是呢,實際應用中,出於種種原因,咱們可不太願意每件事都走得那麼慢,像是緩慢行走的蝸牛一般。所以,可串行化在現實生活中用得並不多。

如何既能儘可能提高隔離性,又提高併發性能?這裏分場景進行 優化,可以 可以分爲兩個場景:

  • 寫寫隔離:(一個事務)寫操作對(另一個事務)寫操作的影響:鎖機制保證隔離性
  • 讀寫隔離:(一個事務)寫操作對(另一個事務)讀操作的影響:MVCC保證隔離性

圖片

事務併發處理的四大場景

MVCC的根本目標:提升併發能力.

首先, 這裏講 事務的併發處理分爲四大場景,分別是

  • 讀-讀
  • 寫-寫
  • 讀-寫
  • 寫-讀

這四種情況分別對應併發事務執行時的四種場景。

讀-讀場景:

讀-讀場景即是指多個事務/線程在併發讀取一個相同的數據,比如事務T1正在讀取ID=16的行記錄,事務T2也在讀取這條記錄,兩個事務之間是併發執行的。

MySQL執行查詢語句,絕對不會對引起數據的任何變化,因此對於這種情況而言,不需要做任何操作,因爲不改變數據就不會引起任何併發問題。

寫-寫場景

寫-寫場景也比較簡單,也就是指多個事務之間一起對同一數據進行寫操作,

比如事務T1ID=16的行記錄做修改操作,事務T2則對這條數據做刪除操作,事務T1提交事務後想查詢看一下,結果連這條數據都不見了,這也是所謂的髒寫問題,也被稱爲更新覆蓋問題

對於這個問題在所有數據庫、所有隔離級別中都是零容忍的存在,最低的隔離級別也要解決這個問題。

讀-寫、寫-讀場景

讀-寫、寫-讀實際上從宏觀角度來看,可以理解成同一種類型的操作,但從微觀角度而言則是兩種不同的情況,

  • 讀-寫是指一個事務先開始讀,然後另一個事務則過來執行寫操作,
  • 寫-讀則相反,主要是讀、寫發生的前後順序的區別。

併發事務中同時存在讀、寫兩類操作時,這是最容易出問題的場景,髒讀、不可重複讀、幻讀都出自於這種場景中,當有一個事務在做寫操作時,讀的事務中就有可能出現這一系列問題,因此數據庫纔會引入各種機制解決。

各併發事務場景的解決方案

對於寫-寫、讀-寫、寫-讀這三類存在線程安全問題的場景,最爲簡單粗暴的方式,通過 加鎖 的方案確保線程安全。

但是,直接加鎖 會導致部分的串行化、整體串行化,因此效率會下降,而MVCC機制的誕生則解決了這個問題。

因此MySQL推出了MVCC機制,在讀-寫並存(讀-寫、寫-讀)的場景,使用局部無鎖架構,提升性能。

MVCC 機制 在線程安全問題和加鎖串行化之間做了一定取捨,讓兩者之間達到了很好的平衡,即防止了髒讀、不可重複讀及幻讀問題的出現又無需對併發讀-寫事務加鎖處理。

首先,來看看寫-寫場景 ,如何通過鎖機制實現事務隔離。

鎖機制實現寫-寫隔離

首先來看兩個事務的寫操作之間的相互影響。

隔離性要求同一時刻只能有一個事務對數據進行寫操作,InnoDB通過鎖機制來保證這一點。

鎖機制的基本原理可以概括爲:

  • 事務在修改數據之前,需要先獲得相應的鎖;

  • 獲得鎖之後,事務便可以修改數據;

  • 該事務操作期間,這部分數據是鎖定的,其他事務如果需要修改數據,需要等待當前事務提交或回滾後釋放鎖。

從操作的粒度進行的mysql鎖的分類

從操作的粒度可分爲表級鎖、行級鎖和頁級鎖。

表級鎖:

每次操作鎖住整張表鎖定粒度大,發生鎖衝突的概率最高,併發度最低

應用在MyISAM、InnoDB、BDB 等存儲引擎中。

表鎖的特點:

  • 開銷小,加鎖快
  • 不會出現死鎖
  • 鎖定粒度大,發生鎖衝突的概率最高,併發度最低

行級鎖:

每次操作鎖住一行數據鎖定粒度最小,發生鎖衝突的概率最低,併發度最高

應用在InnoDB 存儲引擎中。

行鎖的特點:

  • 開銷大,加鎖慢
  • 會出現死鎖
  • 鎖定粒度小,發生鎖衝突的概率最低,併發度最高

頁級鎖:

每次鎖定相鄰的一組記錄,鎖定粒度界於表鎖和行鎖之間,開銷和加鎖時間界於表鎖和行鎖之間,併發度一般。

頁鎖的特點:

  • 開銷和加鎖時間介於表鎖和行鎖之間
  • 會出現死鎖
  • 鎖定粒度介於表鎖和行鎖之間,併發度一般

InnoDB存儲引擎三種行鎖模式

InnoDB引擎行鎖是通過對索引數據頁上的記錄加鎖實現的,主要實現算法有 3 種:Record Lock、Gap Lock 和 Next-key Lock,也就是InnoDB的三種行鎖模式。

  • RecordLock鎖(行鎖):鎖定單個行記錄的鎖。(RecordLock鎖 是記錄鎖,RC、RR隔離級別都支持)
  • GapLock鎖:間隙鎖,鎖定索引記錄間隙(不包括記錄本身),確保索引記錄的間隙不變。(GapLock是範圍鎖,RR隔離級別支持。RC隔離級別不支持)
  • Next-key Lock 鎖(臨鍵鎖):記錄鎖和間隙鎖組合,同時鎖住數據,並且鎖住數據前後範圍。(記錄鎖+範圍鎖,RR隔離級別支持。RC隔離級別不支持)

記錄鎖(Record Locks)

(1)記錄鎖, 僅僅鎖住索引記錄的一行,在單條索引記錄上加鎖。
(2)record lock鎖住的永遠是索引,而非記錄本身,即使該表上沒有任何索引,那麼innodb會在後臺創建一個隱藏的聚集主鍵索引,那麼鎖住的就是這個隱藏的聚集主鍵索引。

所以說當一條sql沒有走任何索引時,那麼將會在每一條聚合索引後面加X鎖,這個類似於表鎖,但原理上和表鎖應該是完全不同的。

間隙鎖(Gap Locks)

(1)區間鎖, 僅僅鎖住一個索引區間(開區間,不包括雙端端點)。
(2)在索引記錄之間的間隙中加鎖,或者是在某一條索引記錄之前或者之後加鎖,並不包括該索引記錄本身。

(3)間隙鎖可用於防止幻讀,保證索引間的不會被插入數據

比如在 100、10000中,間隙鎖的可能值有 (∞, 100),(100, 10000),(10000, ∞),

臨鍵鎖(Next-Key Locks)

(1)record lock + gap lock, 左開右閉區間。

(2)默認情況下,innodb使用next-key locks來鎖定記錄。select … for update
(3)但當查詢的索引含有唯一屬性的時候,Next-Key Lock 會進行優化,將其降級爲Record Lock,即僅鎖住索引本身,不是範圍。
(4)Next-Key Lock在不同的場景中會退化:

在這裏插入圖片描述

比如在 100、10000中,臨鍵鎖(Next-Key Locks)的可能有 (∞, 100],(100, 10000] , 40歲老架構師尼恩提示,這裏的關鍵是左開右閉

具體的講解,請參見《尼恩Java面試寶典》配套視頻。

讀-寫場景,如何實現事務的隔離?

介紹完寫操作之間(寫-寫)的相互影響,下面,來看看讀-寫場景 ,如何通過MVCC實現事務隔離。

讀-寫場景三類問題: 髒讀、不可重複讀和幻讀

首先來看併發情況下,讀操作可能存在的三類問題:

(1)髒讀:當前事務(A)中可以讀到其他事務(B)未提交的數據(髒數據),這種現象是髒讀。

髒讀舉例如下(以賬戶餘額表爲例):

img

(2)不可重複讀:在事務A中先後兩次讀取同一個數據,兩次讀取的結果不一樣,這種現象稱爲不可重複讀。

髒讀與不可重複讀的區別在於:前者讀到的是其他事務未提交的數據,後者讀到的是其他事務已提交的數據。

不可重複讀舉例如下:

img

(3)幻讀:在事務A中按照某個條件先後兩次查詢數據庫,兩次查詢結果的條數不同,這種現象稱爲幻讀。

不可重複讀與幻讀的區別可以通俗的理解爲:前者是數據變了,後者是數據的行數變了。

幻讀舉例如下:

img

以上內容如果不懂,請參考後面的《尼恩Java面試寶典》 配套視頻

事務隔離級別

SQL標準中定義了四種隔離級別,並規定了每種隔離級別下上述幾個問題是否存在。

先來回顧一下,數據庫事務的隔離級別,目前數據庫事務的隔離級別一共有 4 種,由低到高分別爲:

事務的四個隔離級別:

  • 未提交讀(READ UNCOMMITTED):所有事務都可以看到其他事務未提交的修改。一般很少使用;
  • 讀已提交(READ COMMITTED):Oracle默認隔離級別,事務之間只能看到彼此已提交的變更修改;
  • 可重複讀(REPEATABLE READ):MySQL默認隔離級別,同一事務中的多次查詢會看到相同的數據行;可以解決不可重複讀,但可能出現幻讀;
  • 可串行化(SERIALIZABLE):最高的隔離級別,事務串行的執行,前一個事務執行完,後面的事務會執行。讀取每條數據都會加鎖,會導致大量的超時和鎖爭用問題;

隔離級別與讀問題的關係如下:

img

在實際應用中,第一種和第四種很少用:

  • 讀未提交在併發時會導致很多問題,而性能相對於其他隔離級別提高卻很有限,因此使用較少。

  • 可串行化強制事務串行,併發效率很低,只有當對數據一致性要求極高且可以接受沒有併發時使用,因此使用也較少。

因此在大多數數據庫系統中,默認的隔離級別是讀已提交可重複讀(後文簡稱RR)

數據庫一般默認的隔離級別爲 讀已提交 RC ,比如 Oracle,

也有一些數據的默認隔離級別爲 可重複讀 RR,比如 Mysql。

"可重複讀"(Repeatable Read)這個級別確保了對同一字段的多次讀取結果是一致的,除非數據是被本身事務自己所修改。

"可重複讀" RR它能夠防止髒讀、不可重複讀,但可能會遇到幻讀的情況。 不過,mysql用自己的方案解決了RR 幻讀的情況 , 參考 文章:

Mysql如何實現RR級隔離時,不會幻讀?

一般而言,數據庫的讀已提交(READ COMMITTED)能夠滿足業務絕大部分場景了。

所以, 大廠建議將 MySQL默認的Repeatable Read隔離級別,改成了RC隔離級別 , 主要是爲了提升性能, 具體請參考 《尼恩Java 面試寶典》 MYSQL 專題:

在這裏插入圖片描述

可以通過如下兩個命令 查看全局隔離級別 :

img

可以通過如下兩個命令 查看 本次會話的隔離級別:

img

MVCC是 無鎖架構:是COW思想的一種實現

MVCC是一種無鎖架構。

MVCC最大的優點是讀不加鎖,因此讀寫不衝突,併發性能好。

Copy-On-Write(COW,寫時複製)是一種常見的併發編程思想, 主要解決 讀寫併發的問題。

Copy-On-Write基本思想是,當多個線程需要對共享數據進行修改時,不直接在原始數據上進行操作,而是先將原始數據複製一份(即寫時複製),然後在副本上進行Write。

Copy-On-Write 通過操作寫操作副本,引入局部無鎖架構,解決並且處理之間的數據衝突,提高了併發性能。

關於 COW思想 的介紹,請參見 尼恩的 另一篇文章:

MVCC學習聖經:一文穿透MySQL MVCC,吊打面試官

以上內容如果不懂,請參考後面的《尼恩Java面試寶典》 配套視頻

MVCC機制的三個核心組件

MVCC機制主要通過三個組件實現:

  • 隱藏字段
  • Undo-log日誌
  • ReadView

核心組件1. 隱藏字段

在Innodb存儲引擎中,每一行記錄中都有隱藏字段

  • 在有聚簇索引的情況下每一行記錄中都會隱藏3個字段,
  • 如果沒有聚簇索引的情況下每一行記錄中都會隱藏4個字段。

在有聚簇索引的情況下每一行記錄中都會隱藏3個字段爲DB_TRX_ID,DB_ROLL_PTR、deleted_bit,

  • DB_TRX_ID:記錄創建這條數據上次修改它的事務 ID,
  • DB_ROLL_PTR:回滾指針,指向這條記錄的上一個版本
  • deleted_bit字段,即記錄被更新或刪除,這裏的刪除並不代表真的刪除,而是將這條記錄的delete flag改爲true

除了上面的3個隱藏字段,沒有聚簇索引還會有DB_ROW_ID這個字段。

40歲老架構師尼恩提是:隱藏字段的細節,稍後詳細介紹。

核心組件2. undo log(回滾日誌)

在事務的ACID特性中,undo log(回滾日誌)主要用於實現事務的原子性、隔離性、一致性的關鍵組件之一。它的主要作用包括:

  1. 事務的回滾操作

    當一個事務執行過程中發生錯誤或者被用戶顯式回滾時,數據庫系統需要能夠撤銷該事務已經執行的操作,將數據庫恢復到事務開始之前的狀態。這就是回滾操作。

    undo log記錄了事務執行過程中所做的所有修改操作的逆操作,通過undo log可以快速回滾事務所做的修改,從而保證事務的原子性。

  2. 恢復和崩潰恢復

    當數據庫系統發生崩潰或者異常關閉時,可能會導致部分事務未提交的修改操作丟失或者部分已提交的修改操作未持久化到磁盤。

    通過undo log,數據庫系統可以在恢復過程中, 將未提交的修改操作回滾,並將已提交但未持久化的修改操作重新應用到數據庫中,從而保證數據庫的一致性和完整性。

總的來說,undo log在數據庫系統中扮演着非常重要的角色,它不僅用於實現事務的回滾操作和併發控制,還用於數據庫系統的恢復和崩潰恢復。通過記錄事務的修改操作和逆操作,undo log確保了數據庫的原子性、隔離性和一致性,是數據庫系統的關鍵組件之一。

尼恩在前面講到, MVCC 實現了自己 Copy-On-Write思想提升併發能力的時候, 也需要數據的副本,這裏既然undo-log 有了那麼多副本,MVCC 就借雞生蛋, 複用 這些數據副本。

所以,undo log 中的副本,可以用於實現多版本併發控制(MVCC),提升事務的併發性能。

核心組件3. read-view

那麼多的數據副本,通過對比時間戳或者版本號,看到自己能看的版本?

undo log保存的是一個版本鏈,也就是使用DB_ROLL_PTR這個字段來連接的。

多個事務的 undo-log 日誌副本 (數據快照),組成了一個 副本鏈,如下圖:

那麼,如果多個事務並行的讀寫操作,每一個事務應該使用那個版本呢?

MVCC 使用 一個新的組件,read-view + 一組對比規則,來計算 可見版本。

read-view 有一些列的對比規則,這些規則用於確定一個事務在讀取數據時,如何與數據庫中的其他事務的版本號(這裏其實就是事務ID)進行比較,以確定它所能看到的數據版本。

當 執行一個select語句時MVCC 會產生一致性視圖read view。那麼這個read view 沒有記錄事務的開始時間,和截止時間 , 而是換成另一種方式去記錄開始時間和截止時間,換成什麼方式呢:

  • read view 記錄當前活躍事務 id,組成活躍事務id數組 ,這個屬性的作用,哪些事務是當前事務,也是不可見的
  • read view 記錄當前最小活躍事務 id,這個屬性的作用,用於判斷哪些事務是已經提交了的
  • read view 記錄當前的下一個事務 id,這個屬性的作用,用於判斷哪些事務是未來事務,也是不可見的

以上內容如果不懂,請參考後面的《尼恩Java面試寶典》 配套視頻

注意,上面是尼恩爲大家總結和歸納的,比較清晰好記, mysql 的MVCC 版本的對比規則, 看上去非常、非常複雜。

下面是mysql 的MVCC 的read view 版本對比規則, 確實也是一個非常複雜的對比邏輯, 很多小夥伴傻傻看不懂, 並且背誦了半天還記不住,非常痛苦。

通過 上面的這個複雜的對比流程, read-view 終於確定一個事務在執行時所能看到的數據視圖。

但是上面這種圖很複雜,網上有上萬篇文章 抄來抄去, 沒有一篇文章做了總結和簡化。

這個對比規則 邏輯複雜,導致儘管大家看了那些文章,甚至看了很多視頻,還是不能理解透徹, 迷迷糊糊的,面試的時候 說不清楚,也很容易忘了。

尼恩團隊看不下去,用咱們的雄厚技術實力(洪荒之力), 給大家來總結和簡化。 具體如下:

此圖,是全網的第一張徹底穿透式的解讀 MVCC的對比規則的圖。

通過此文,尼恩團隊 第一次,幫助大家搞清楚複雜的 MVCC的底層原理。

此圖很容易理解,很容易記憶。

大家可收藏起來, 面試之前複習一下,一定能吊打面試官。不對,是吊死面試官。

以上內容如果不懂,請參考後面的《尼恩Java面試寶典》 配套視頻

和上面的圖類似, 尼恩團隊用 深厚的架構功力 把一些其他的複雜的問題做了穿透式、起底式的分析, 比如:

  • 比如Netty的內存池和對象池(那個超級難,很多人窮其一生都搞不懂),
  • 比如DDD的建模和落地,
  • 比如Caffeine的底層架構,
  • 比如高性能葵花寶典
  • 比如 Thread Local 學習聖經
  • 等等等等。

這個技術難題一旦掌握,大家內力猛漲。 所以,建議大家去看看尼恩的這些核心內容。

扯遠了,言歸正傳。

快照讀和當前讀

  • 快照讀(也稱普通讀,英文名:Consistent Read)

  • 當前讀(也稱鎖定讀,Locking Read)

快照讀,就是讀取快照數據,即快照生成的那一刻的數據。

在不加鎖的情況下,我們使常用的 普通的SELECT語句 就是快照讀,如下:

SELECT * FROM USER WHERE ......

當前讀,就是讀取最新的數據,要讀取最新提交的數據版本。

我們在加鎖SELECT語句,或者對數據進行增、刪、改都會進行當前讀。如下:

SELECT * FROM USER LOCK IN SHARE MODE;

SELECT * FROM USER FOR UPDATE;

INSERT INTO USER VALUES ......

DELETE FROM USER WHERE ......

UPDATE USER SET ......

在MySQL中只有在 RR 和 RC 這兩個事務隔離級別下才會使用 快照讀

在RR中,快照會在事務中第一次SELECT語句執行時生成,只有在本事務中對數據進行更改 纔會更新快照。

在RC中,每次SELECT都會重新生成一個快照,總是讀取最新版本數據。

RR解決髒讀、不可重複讀、幻讀等問題,使用的是MVCC:MVCC。

快照讀,就是讀取快照數據,即快照生成的那一刻的數據。

在同一時刻,不同的事務讀取到的數據可能是不同的(即多版本 數據快照 )——在T5時刻,事務A和事務C可以讀取到不同版本的數據。

img

RR隔離級別的非加鎖讀

下面以RR隔離級別爲例,結合前文提到的幾個問題分別說明。

(1)髒讀

img

當事務A在T3時刻讀取zhangsan的餘額前,會生成ReadView,由於此時事務B沒有提交仍然活躍,因此其事務id一定在ReadView的rw_trx_ids中,

因此根據前面介紹的規則,事務B的修改對ReadView不可見。

接下來,事務A根據指針指向的undo log查詢上一版本的數據,得到zhangsan的餘額爲100。

這樣事務A就避免了髒讀。

(2)不可重複讀

img

當事務A在T2時刻讀取zhangsan的餘額前,會生成ReadView。

此時事務B分兩種情況討論,一種是如圖中所示,事務已經開始但沒有提交,此時其事務id在ReadView的rw_trx_ids中;一種是事務B還沒有開始,此時其事務id大於等於ReadView的low_limit_id。

無論是哪種情況,根據前面介紹的規則,事務B的修改對ReadView都不可見。

當事務A在T5時刻再次讀取zhangsan的餘額時,會根據T2時刻生成的ReadView對數據的可見性進行判斷,從而判斷出事務B的修改不可見;

因此事務A根據指針指向的undo log查詢上一版本的數據,得到zhangsan的餘額爲100,從而避免了不可重複讀。

(3)幻讀

img

MVCC避免幻讀的機制與避免不可重複讀非常類似。

當事務A在T2時刻讀取0<id<5的用戶餘額前,會生成ReadView。

此時事務B分兩種情況討論,一種是如圖中所示,事務已經開始但沒有提交,此時其事務id在ReadView的rw_trx_ids中;一種是事務B還沒有開始,此時其事務id大於等於ReadView的low_limit_id。

無論是哪種情況,根據前面介紹的規則,事務B的修改對ReadView都不可見。

當事務A在T5時刻再次讀取0<id<5的用戶餘額時,會根據T2時刻生成的ReadView對數據的可見性進行判斷,從而判斷出事務B的修改不可見。

因此對於新插入的數據lisi(id=2),事務A根據其指針指向的undo log查詢上一版本的數據,發現該數據並不存在,從而避免了幻讀。

讀已提交(RC)隔離級別下的非加鎖讀

前面介紹的MVCC,是RR隔離級別下“非加鎖讀”實現隔離性的方式。

(1)讀已提交(RC)隔離級別下的非加鎖讀

RC與RR一樣,都使用了MVCC,其主要區別在於:

RR是在事務開始後第一次執行select前創建ReadView,直到事務提交都不會再創建。根據前面的介紹,RR可以避免髒讀、不可重複讀和幻讀。

RC每次執行select前都會重新建立一個新的ReadView,因此如果事務A第一次select之後,事務B對數據進行了修改並提交,那麼事務A第二次select時會重新建立新的ReadView,因此事務B的修改對事務A是可見的。

因此RC隔離級別可以避免髒讀,但是無法避免不可重複讀和幻讀。

(2)加鎖讀與next-key lock

按照是否加鎖,MySQL的讀可以分爲兩種:

一種是非加鎖讀,就是快照讀、一致性讀,使用普通的select語句,這種情況下使用MVCC避免了髒讀、不可重複讀、幻讀,保證了隔離性。

另一種是加鎖讀,查詢語句有所不同,如下所示:

SELECT * FROM USER LOCK IN SHARE MODE;

SELECT * FROM USER FOR UPDATE;

加鎖讀在查詢時會對查詢的數據加鎖(共享鎖或排它鎖)。

由於鎖的特性,當某事務對數據進行加鎖讀後,其他事務無法對數據進行寫操作,因此可以避免髒讀和不可重複讀。

而避免幻讀,則需要通過next-key lock。

next-key lock 是行鎖的一種,實現相當於record lock(記錄鎖) + gap lock(間隙鎖);其特點是不僅會鎖住記錄本身(record lock的功能),還會鎖定一個範圍(gap lock的功能)。

因此,加鎖讀同樣可以避免髒讀、不可重複讀和幻讀,保證隔離性。

以上內容如果不懂,請參考後面的《尼恩Java面試寶典》 配套視頻

隔離級別、併發性、數據一致性的三角之間關係

一圖勝千言,40歲老架構師用一張圖,給大家總結一下 事務隔離級別、併發性、數據一致性的三角之間關係:

事務隔離級別和併發性和數據一致性密切相關。不同的隔離級別提供了不同的併發性和數據一致性保證。

  1. 併發性
    • 併發性指的是數據庫系統同時處理多個事務的能力。隔離級別越低,允許的併發操作越多,系統的併發性能越高。
    • 但是,過高的併發操作可能會導致事務之間的相互干擾,產生一些併發問題,如髒讀、不可重複讀和幻讀。
  2. 數據一致性
    • 數據一致性指的是事務執行後,數據庫中的數據是否保持一致性。隔離級別越高,數據一致性越好,但對併發操作的限制也越嚴格。
    • 高隔離級別可以防止一些併發問題的產生,如髒讀、不可重複讀和幻讀,但會降低系統的併發性能。

參考閱讀一下 尼恩的幾篇相關文章:

Mysql如何實現RR級隔離時,不會幻讀?

阿里面試:Seata 如何實現 RC ?保證事務的隔離性?

以上內容如果不懂,請參考後面的《尼恩Java面試寶典》 配套視頻

隔離性的實現總結

概括來說,InnoDB實現的RR,通過鎖機制(包含next-key lock)、MVCC(隱藏列+undo log的版本鏈+ReadView)等,實現了一定程度的隔離性,可以滿足大多數場景的需要。

不過需要說明的是,RR雖然避免了幻讀問題,但是畢竟不是Serializable,不能保證完全的隔離,下面是兩個例子:

第一個例子,如果在事務中第一次讀取採用非加鎖讀,第二次讀取採用加鎖讀,則如果在兩次讀取之間數據發生了變化,兩次讀取到的結果不一樣,因爲加鎖讀時不會採用MVCC。

第二個例子,如下所示,大家可以自己驗證一下。

img

ACID的 C 一致性如何實現?

一致性基本概念

一致性是指事務執行結束後,數據庫的完整性約束沒有被破壞,事務執行的前後都是合法的數據狀態。

數據庫的完整性約束包括但不限於:實體完整性(如行的主鍵存在且唯一)、列完整性(如字段的類型、大小、長度要符合要求)、外鍵約束、用戶自定義完整性(如轉賬前後,兩個賬戶餘額的和應該不變)。

ACID的一致性是指在數據庫事務執行過程中,數據庫從一個一致的狀態轉移到另一個一致的狀態。這意味着事務的執行不會破壞數據庫的完整性約束,數據庫的約束和規則在事務開始和結束時都保持一致。

一致性實現

可以說,一致性是事務追求的最終目標:前面提到的原子性、持久性和隔離性,都是爲了保證數據庫狀態的一致性。

此外,除了數據庫層面的保障,一致性的實現也需要應用層面進行保障。

實現一致性的實現主要依賴於以下幾個方面:

  1. 約束和規則:數據庫中通常定義了一系列的約束和規則,如主鍵約束、外鍵約束、唯一性約束、默認值約束等。在事務執行過程中,數據庫系統會確保所有的更新操作都符合這些約束和規則,以維護數據庫的一致性。
  2. 事務的原子性:在數據庫事務中,原子性確保了事務內的所有操作要麼全部成功執行,要麼全部失敗回滾。這確保了數據庫在事務執行期間保持一致狀態,不會出現部分完成的情況。
  3. 併發控制:數據庫系統通過併發控制機制來管理多個併發事務對數據庫的訪問。這確保了在併發執行的多個事務之間保持數據的一致性。例如,通過鎖機制、隔離級別等方式來確保事務的隔離性和一致性。
  4. 事務日誌:事務日誌記錄了事務執行過程中的所有操作,包括更新操作和撤銷操作。事務日誌可以用於在系統故障後恢復數據庫到一致狀態,確保事務的一致性。
  5. 數據備份和恢復:定期對數據庫進行備份,並確保備份數據的完整性和一致性。在系統發生故障或者數據損壞時,可以通過備份數據來恢復數據庫,確保數據的一致性。

通過以上機制的配合,數據庫系統可以確保事務的執行過程中維護數據庫的一致性。

這些機制保證了在任何情況下,數據庫都能從一個一致的狀態轉移到另一個一致的狀態,不會破壞數據庫的完整性約束和規則。

ACID特性實現的總結

下面總結一下ACID特性及其實現原理:

  • 原子性:語句要麼全執行,要麼全不執行,是事務最核心的特性,事務本身就是以原子性來定義的;實現主要基於undo log
  • 持久性:保證事務提交後不會因爲宕機等原因導致數據丟失;實現主要基於redo log
  • 隔離性:保證事務執行儘可能不受其他事務影響;InnoDB默認的隔離級別是RR,RR的實現主要基於鎖機制(包含next-key lock)、MVCC(包括數據的隱藏列、基於undo log的版本鏈、ReadView)
  • 一致性:事務追求的最終目標,一致性的實現既需要數據庫層面的保障,也需要應用層面的保障

以上內容如果不懂,請參考後面的《尼恩Java面試寶典》 配套視頻

參考文獻

MVCC學習聖經:一文穿透MySQL MVCC,吊打面試官

Mysql如何實現RR級隔離時,不會幻讀?

美團一面:聊聊MySQL的七種日誌

說在最後:有問題找老架構取經

MVCC 、ACID如何實現相關的面試題,是非常常見的面試題。也是核心面試題。

以上的內容,如果大家能對答如流,如數家珍,基本上 面試官會被你 震驚到、吸引到。最終,讓面試官愛到 “不能自已、口水直流”。offer, 也就來了。

在面試之前,建議大家系統化的刷一波 5000頁《尼恩Java面試寶典》V174,在刷題過程中,如果有啥問題,大家可以來 找 40歲老架構師尼恩交流。

另外,如果沒有面試機會,可以找尼恩來幫扶、領路。尼恩已經指導了大量的就業困難的小夥伴上岸.

前段時間,幫助一個40歲+就業困難小夥伴拿到了一個年薪100W的offer,小夥伴實現了 逆天改命

另外,尼恩也給一線企業提供 《DDD 的架構落地》企業內部培訓,目前給不少企業做過內部的諮詢和培訓,效果非常好。

![img/10001.jpg)

技術自由的實現路徑:

實現你的 架構自由:

喫透8圖1模板,人人可以做架構

10Wqps評論中臺,如何架構?B站是這麼做的!!!

阿里二面:千萬級、億級數據,如何性能優化? 教科書級 答案來了

峯值21WQps、億級DAU,小遊戲《羊了個羊》是怎麼架構的?

100億級訂單怎麼調度,來一個大廠的極品方案

2個大廠 100億級 超大流量 紅包 架構方案

… 更多架構文章,正在添加中

實現你的 響應式 自由:

響應式聖經:10W字,實現Spring響應式編程自由

這是老版本 《Flux、Mono、Reactor 實戰(史上最全)

實現你的 spring cloud 自由:

Spring cloud Alibaba 學習聖經》 PDF

分庫分表 Sharding-JDBC 底層原理、核心實戰(史上最全)

一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之間混亂關係(史上最全)

實現你的 linux 自由:

Linux命令大全:2W多字,一次實現Linux自由

實現你的 網絡 自由:

TCP協議詳解 (史上最全)

網絡三張表:ARP表, MAC表, 路由表,實現你的網絡自由!!

實現你的 分佈式鎖 自由:

Redis分佈式鎖(圖解 - 秒懂 - 史上最全)

Zookeeper 分佈式鎖 - 圖解 - 秒懂

實現你的 王者組件 自由:

隊列之王: Disruptor 原理、架構、源碼 一文穿透

緩存之王:Caffeine 源碼、架構、原理(史上最全,10W字 超級長文)

緩存之王:Caffeine 的使用(史上最全)

Java Agent 探針、字節碼增強 ByteBuddy(史上最全)

實現你的 面試題 自由:

4800頁《尼恩Java面試寶典 》 40個專題

免費獲取11個技術聖經PDF:

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