MySQL學習之mvcc原理和當前讀

2021-03-02 白砂

閱讀本文大約需要 20 分鐘。概念多一點, 請耐心讀完。

之前學習了MySQL事務隔離級別,在可重複讀的隔離級別下,如果一個事務啟動的時候,會創建一個視圖 read view,之後這個事務在執行期間,即使有其他事務修改了數據,這個事務看到的仍然和啟動時是一樣的,好像別人做的任何操作和他無關一樣。
首先先了解一下事物的啟動時機和視圖概念是怎麼樣的吧。第一種啟動方式,一致性視圖是在執行第一個快照讀語句時創建的。begin/start transaction 命令不是一個事務的起點,在執行它們之後的第一個操作InnoDB表的語句,事務才是真正的啟動。第二種啟動方式,一致性視圖是在執行 start transaction with consistent snapshot 時創建的。start transaction with consistent snapshot 是馬上啟動事務。
生成一致性事務這個操作是任意DML操作都可以嗎?select 也會生成一致性視圖嗎?

普通視圖,view。它是一個查詢語句定義的虛擬表,在調用的時候執行查詢語句並生成結果。

語法:CREATE VIEW 視圖名 AS SELECT 查詢語句。

InnoDB在實現MVCC時用到的一致性讀視圖,consistent read view。用於支持RC(read committed, 讀提交) 和 RR(repeatable read, 可重複讀)隔離級別的實現。

在之前的事務隔離中筆記中,我們知道,視圖的概念只存在於可重複讀和讀提交這兩個隔離級別中在可重複讀的級別下,事務在啟動的時候,就「拍了快照」,而且這個快照是基於整個庫的。


InnoDB 每個事務都有一個唯一的事務ID,叫做 transaction id。事務id是在事務開始的時候向InnoDB 的事務系統申請的,是按申請順序嚴格遞增的。


對於使用InnoDB存儲引擎的表來說,它的聚簇索引記錄中都包含兩個必要的隱藏列(row_id並不是必要的,我們創建的表中有主鍵或者非NULL的UNIQUE鍵時都不會包含row_id列)
也就是說,數據表中一行數據記錄,其實可能有多個版本,每個版本都有自己的row trx_id。

(一行記錄狀態變更  示意圖)

圖中V1至V4是4個版本, 當前最新版本是V4,K的值是22,它是被transaction id 為 25 的事務更新的, 因此它的trx_id也是25。
語句操作數據更新時會生成undo log(回滾日誌),而V1, V2, V3 並不是物理上真是存在的,而是每次需要的時候根據當前版本和undo log 計算出來的。如何進行計算呢?每條回滾日誌都有一個 roll_pointer 屬性(insert 操作對應的undo log 沒有該屬性, 因為該記錄沒有更早的版本), 可以將這個undo 日誌都連起來, 串成一個鍊表。

對該記錄每次更新後,都會將舊值放到一條undo log 日誌中,就算是該記錄的一個舊版本,隨著更新次數的增多,所有的版本都會被 roll_pointer 屬性連成一個鍊表, 這個鍊表成為版本鏈, 版本鏈頭部節點則是當前記錄的最新值。如果需要進行找到回滾的undo log,則通過版本鏈進行一個個的查找, 直到找到目的版本。



一個事務啟動的時候,能夠看到所有已經提交的事務。但是之後,這個事務執行期間,其他事物的更新對他不可見。
因此一個事務只需要在啟動的時候說, 以我啟動的時刻為準,如果一個數據版本在我啟動之前生成的,就是可見的,如果在啟動以後生成的,就是不可見,我必須找到他的上一個版本。如果上一個版本還是不可見的,就繼續往上找。如果說是我自己更新的數據,在此是可見的。
其實核心問題就是:需要判斷一下版本鏈中哪個版本是當前事務可見的?

m_ids:表示在生成read-view時,也就是這個事務啟動瞬間,當前正在「活躍」的所有事務ID 的數組。活躍 -- 啟動了但還沒提交的事務。

min_trx_id:表示在生成read-view時當前系統中「活躍」的事務ID中最小的事務ID,也就是m_ids中最小值。

max_trx_id:表示生成read-view時系統中應該分配給下一個事務的id值

tips:注意max_trx_id並不是m_ids中的最大值,事務id是遞增分配的。比方說現在有id為1,2,3這三個事務,之後id為3的事務提交了。那麼一個新的讀事務在生成read-view時,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。

creator_trx_id:表示在生成read-view 事務的事務ID。就是當前操作生成的事務 的 事務ID。

tips:只有對表中的數據記錄做改動時(執行insert, delete, update這些語句時)才會生成事務id,否則在一個只讀事務中的事務id值默認都為0。

我們把事務ID中的min_trx_id記為低水位,max_trx_id即為高水位。這個視圖數組和高水位就組成了當前事務的一致性視圖。而數據版本的可見性規則,就是基於數據的 row trx_id 和 這個一致性視圖的對比結果得到的。

如果被訪問版本的 trx_id 屬性值 與 read-view 中 createor_trx_id 值相同,意味著當前事務在訪問自己修改過的記錄,所以該版本是可見的。

如果被訪問版本的 trx_id 屬性值 小於 read-view 中 min_trx_id 的值,意味著生成該版本的事務在當前事務生成 read-view 之前已經提交,所以該版本是可見的。

如果被訪問版本的 trx_id 屬性值 大於或等於 read-view 中 max_trx_id 的值,意味著生成該版本的事務在當前事務生成 read-view 之後才開啟(生成的),所以該版本是不可見的。

如果被訪問的版本 trx_id 屬性值在 read-view 的 min_trx_id 和 max_trx_id 之間,這需要判斷 trx_id 屬性值是不是在 m_ids 數組中。如果存在,說明創建生成 read-view 時生成該版本的事務還是活躍的,該版本是不可見的。如果不存在,說明創建生成 read-view 時生成該版本的事務已經被提交了,所以該版本是可見的。

(數據版本可見性規則  示意圖)


如果落在綠色部分,表示這個版本是已提交的事務或者是當前事務自己生成的,這個數據是可見的;

如果落在紅色部分,表示這個版本是由將來啟動的事務生成的,是肯定不可見的;

如果落在黃色部分,那就包括兩種情況

a. 若 row trx_id 在數組中,表示這個版本是活躍的還沒提交的事務生成的,不可見;

b. 若 row trx_id 不在數組中,表示這個版本是已經提交了的事務生成的,可見。

比方說現在系統裡有一張hero表,假設現在表中只有一個事務id為80的事務插入一條記錄。
mysql> SELECT * FROM hero;+---+---+----+| number | name   | country |+---+---+----+|      1 | 劉備   | 蜀      |+---+---+----+

接下來看一下READ COMMITTED和REPEATABLE READ所謂的生成read-view的時機不同到底不同在哪裡。

讀提交 -- 每次讀取數據前都生成一個 read-view

比方說現在系統裡有兩個事物id,分別是100,200的事務在進行:
# Transaction 100BEGIN;UPDATE hero SET name = '關羽' WHERE number = 1;UPDATE hero SET name = '張飛' WHERE number = 1;

# Transaction 200BEGIN;# 更新了一些別的表的記錄...

tips: 事務執行過程中,只有在第一次真正修改記錄時(比如使用INSERT、DELETE、UPDATE語句),才會被分配一個單獨的事務id,這個事務id是遞增的。在Transaction 200中更新一些別的表的記錄,目的是讓它分配事務id。

(hero 表中 number 為1的記錄 版本鍊表  示意圖)

# 使用READ COMMITTED隔離級別的事務BEGIN;# SELECT1:Transaction 100、200未提交SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'劉備'

在執行SELECT語句時會先生成一個read-view,read-view的m_ids列表的內容就是[100, 200],min_trx_id為100,max_trx_id為201,creator_trx_id為0。

然後從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內容是'張飛',該版本的trx_id值為100,在m_ids列表內,所以不符合可見性要求,根據roll_pointer跳到下一個版本。

下一個版本的列name的內容是'關羽',該版本的trx_id值也為100,也在m_ids列表內,所以也不符合要求,繼續跳到下一個版本。

下一個版本的列name的內容是'劉備',該版本的trx_id值為80,小於read-view中的min_trx_id值100,所以這個版本是符合要求的,最後返回給用戶的版本就是這條列name為'劉備'的記錄。

# Transaction 100BEGIN;UPDATE hero SET name = '關羽' WHERE number = 1;UPDATE hero SET name = '張飛' WHERE number = 1;COMMIT;

然後再到事務id為200的事務中更新一下表hero中number為1的記錄:
# Transaction 200BEGIN;# 更新了一些別的表的記錄...UPDATE hero SET name = '趙雲' WHERE number = 1;UPDATE hero SET name = '諸葛亮' WHERE number = 1;

(hero 表中 number 為1的記錄 版本鍊表  示意圖)然後再到剛才使用READ COMMITTED隔離級別的事務中繼續查找這個number為1的記錄,如下:
# 使用READ COMMITTED隔離級別的事務BEGIN;# SELECT1:Transaction 100、200均未提交SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'劉備'# SELECT2:Transaction 100提交,Transaction 200未提交SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'張飛'

• 在執行SELECT語句時會又會單獨生成一個read-view,該read-view的m_ids列表的內容就是[200](事務id為100的那個事務已經提交了,所以再次生成快照時就沒有它了),min_trx_id為200,max_trx_id為201,creator_trx_id為0。• 然後從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內容是'諸葛亮',該版本的trx_id值為200,在m_ids列表內,所以不符合可見性要求,根據roll_pointer跳到下一個版本。• 下一個版本的列name的內容是'趙雲',該版本的trx_id值為200,也在m_ids列表內,所以也不符合要求,繼續跳到下一個版本。• 下一個版本的列name的內容是'張飛',該版本的trx_id值為100,小於read-view中的min_trx_id值200,所以這個版本是符合要求的,最後返回給用戶的版本就是這條列name為'張飛'的記錄。以此類推,如果之後事務id為200的記錄也提交了,再次在使用READ COMMITTED隔離級別的事務中查詢表hero中number值為1的記錄時,得到的結果就是'諸葛亮'了。總結一下就是:使用READ COMMITTED隔離級別的事務在每次查詢開始時都會生成一個獨立的read-view

可重複讀 -- 在第一次讀取數據時生成一個read-view

使用REPEATABLE READ隔離級別的事務來說,只會在第一次執行查詢語句時生成一個read-view,之後的查詢就不會重複生成了。

       舉例:

比方說現在系統裡有兩個事務id分別為100、200的事務在執行:
# Transaction 100BEGIN;UPDATE hero SET name = '關羽' WHERE number = 1;UPDATE hero SET name = '張飛' WHERE number = 1;

# Transaction 200BEGIN;# 更新了一些別的表的記錄...

(hero 表中 number 為1的記錄 版本鍊表  示意圖)

假設現在有一個使用REPEATABLE READ隔離級別的事務開始執行:
# 使用REPEATABLE READ隔離級別的事務BEGIN;# SELECT1:Transaction 100、200未提交SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'劉備'

SELECT1的執行過程如下:

• 在執行SELECT語句時會先生成一個read-view,read-view的m_ids列表的內容就是[100, 200],min_trx_id為100,max_trx_id為201,creator_trx_id為0。

• 然後從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內容是'張飛',該版本的trx_id值為100,在m_ids列表內,所以不符合可見性要求,根據roll_pointer跳到下一個版本。

• 下一個版本的列name的內容是'關羽',該版本的trx_id值也為100,也在m_ids列表內,所以也不符合要求,繼續跳到下一個版本。

• 下一個版本的列name的內容是'劉備',該版本的trx_id值為80,小於read-view中的min_trx_id值100,所以這個版本是符合要求的,最後返回給用戶的版本就是這條列name為'劉備'的記錄。

把事務id 為 100 的事務提交一下:

# Transaction 100BEGIN;UPDATE hero SET name = '關羽' WHERE number = 1;UPDATE hero SET name = '張飛' WHERE number = 1;COMMIT;

然後再到事務id為200的事務中更新一下表hero中number為1的記錄:

# Transaction 200BEGIN;# 更新了一些別的表的記錄...UPDATE hero SET name = '趙雲' WHERE number = 1;UPDATE hero SET name = '諸葛亮' WHERE number = 1;

(hero 表中 number 為1的記錄 版本鍊表  示意圖)

然後再到剛才使用REPEATABLE READ隔離級別的事務中繼續查找這個number為1的記錄

# 使用REPEATABLE READ隔離級別的事務BEGIN;# SELECT1:Transaction 100、200均未提交SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'劉備'# SELECT2:Transaction 100提交,Transaction 200未提交SELECT * FROM hero WHERE number = 1; # 得到的列name的值仍為'劉備'

• 因為當前事務的隔離級別為REPEATABLE READ,而之前在執行SELECT1時已經生成過read-view了,所以此時直接復用之前的read-view,之前的read-view的m_ids列表的內容就是[100, 200],min_trx_id為100,max_trx_id為201,creator_trx_id為0。• 然後從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內容是'諸葛亮',該版本的trx_id值為200,在m_ids列表內,所以不符合可見性要求,根據roll_pointer跳到下一個版本。• 下一個版本的列name的內容是'趙雲',該版本的trx_id值為200,也在m_ids列表內,所以也不符合要求,繼續跳到下一個版本。• 下一個版本的列name的內容是'張飛',該版本的trx_id值為100,而m_ids列表中是包含值為100的事務id的,所以該版本也不符合要求,同理下一個列name的內容是'關羽'的版本也不符合要求。繼續跳到下一個版本。• 下一個版本的列name的內容是'劉備',該版本的trx_id值為80,小於read-view中的min_trx_id值100,所以這個版本是符合要求的,最後返回給用戶的版本就是這條列c為'劉備'的記錄。也就是說兩次SELECT查詢得到的結果是重複的,記錄的列c值都是'劉備',這就是可重複讀的含義。

事務 A 開始前,系統裡面只有一個活躍事務 ID 是 99;

事務 A、B、C 的版本號分別是 100、101、102,且當前系統裡只有這四個事務;

三個事務開始前,(1,1)這一行數據的 row trx_id 是 90。

事務 A 的視圖數組就是[99,100], 事務 B 的視圖數組是[99,100,101], 事務 C 的視圖數組是[99,100,101,102]。

事務C先把數據(1,1)改為(1,2), 這個數據最新版本的row trx_id 是 102, 90這個版本已經變為歷史版本.
事務B把數據從 (1,2) 改成了 (1,3)。這時候,這個數據的最新版本(即 row trx_id)是 101,而 102 又成為了歷史版本。在這裡我有一個疑問, 就是如果按照一致性讀的話, 這裡好像是不對的。事務B 事務數組是先生成的,之後C才提交,不應該看見(1,2)的,為什麼結果是(1,3)?如果在事務B在更新之前查詢一次數據,這個查詢返回的結果確實是1。但是,當他要去更新數據的時候,就不能再歷史版本上更新了,否則事務C的更新就丟失了。
這裡會用到一個規則,更新數據都是先讀後寫的,而這個讀,只能讀當前的值,稱之為當前讀。觸發當前讀除了 update 語句外,select 語句如果加鎖,也是當前讀。
所以,在執行事務 B 查詢語句的時候,一看自己的版本號是 101,最新數據的版本號也是 101,是自己的更新,可以直接使用,所以查詢得到的 k 的值是 3。

元宵節了

祝大家

所求皆所願

所盼皆所期

燈火璀璨

萬家平安

最後,求關注。每天進步一點點,歡迎關注我的公眾號「白砂」。

如果我的文章對你有所幫助,還請幫忙點讚、在看、轉發一下,非常感謝!


相關焦點

  • MySQL優化原理分析及優化方案總結
    與之相反的是,伺服器響應給用戶的數據通常會很多,由多個數據包組成。但是當伺服器響應客戶端請求時,客戶端必須完整的接收整個返回結果,而不能簡單的只取前面幾條結果,然後讓伺服器停止發送。因而在實際開發中,儘量保持查詢簡單且只返回必需的數據,減小通信間數據包的大小和數量是一個非常好的習慣,這也是查詢中儘量避免使用 SELECT*以及加上 LIMIT限制的原因之一。
  • MySQL慢查詢記錄原理和內容解析
    作者:高鵬(網名八怪),《深入理解MySQL主從原理32講》系列文的作者。
  • mysql之安裝和遠程登入操作學習總結
    在寫今天關於資料庫的文章之前,還是說一下這近一年來做筆記的感受,我通過這種學習方式:一邊學習一邊總結筆記,日後方便查看和理解;這一點在我從零基礎學習c語言和Linux應用上體現的淋漓盡致,從平時和大家的溝通交流,我能夠去通過以前寫的筆記,做到溫故而知新,同時再給網友講解裡面的原理的時候,又進一步加深了對該知識的理解。
  • 阿里面試:說說一致性讀實現原理?
    這個問題是我當初在面天貓的時候,2面的面試官問我的,我之前已經寫過mvcc的文章了,但是在看到我筆記的裡的這個問題的時候我準備單獨理一遍,所以就有了這個文章。現在,主流關係型資料庫產品基本都實現了MVCC的特性,快照在MVCC中起著重要的作用,代表某一時刻數據的版本,它是實現一致性讀的基礎。在更新操作沒提交前,數據的前鏡像存儲在Undo中,利用Undo可以實現一致性讀,事務回滾以及異常恢復等操作,下面就聊聊MySQL事務,MVCC,快照及一致讀的原理與實現。
  • MySQL中的當前讀
    注意:在MySQL中,修復幻讀的時候,並沒有使用到串行化的事務隔離級別,而是使用了MVCC多版本並發控制和Next key lock臨鍵鎖的方式來修復幻讀問題的。那麼什麼是當前讀呢?這個是什麼意思?接下來我們一起來研究一下。
  • MySQL 工作、底層原理,看這一篇就夠了!
    mysql原理圖各個組件說明:1. connectors與其他程式語言中的sql 語句進行交互,如php、java等。2. Management Serveices & Utilities系統管理和控制工具3.
  • MySQL如何查詢當前正在運行的SQL語句
    queries這一項,如果值長時間>0,說明有查詢執行時間過長 以下為引用的內容: mysql> status; -------------- mysql Ver 11.18 Distrib 3.23.58, for redhat-linux-gnu (i386) Connection id: 53 Current database: (null) Current user: root@localhost Current pager: stdout
  • 高性能Mysql主從架構的複製原理及配置詳解
    而mysqlbinlog對於基於語句的日誌處理十分方便。但是,基於語句的複製並不是像它看起來那麼簡單,因為一些查詢語句依賴於master的特定條件,例如,master與slave可能有不同的時間。所以,MySQL的二進位日誌的格式不僅僅是查詢語句,還包括一些元數據信息,例如,當前的時間戳。即使如此,還是有一些語句,比如,CURRENT USER函數,不能正確的進行複製。
  • 終於學會了 MySQL 主從配置和讀寫分離
    當主節點出現問題的時候要切換到備份節點,切換方式又分為手動切換和自動切換。手動切換具有一定的延時,當主節點出現問題時,只能等運維人員發現或者收到系統通知。主從模式主從配置一般都是和讀寫分離相結合,主伺服器負責寫數據,從伺服器負責讀數據,並保證主伺服器的數據及時同步到從伺服器。
  • MySQL教程之MySQL定時備份資料庫
    >#MySQLdump常用mysqldump -u root -p --databases 資料庫1 資料庫2 > xxx.sql1.2、 mysqldump常用操作示例1.備份全部資料庫的數據和結構mysqldump -uroot -
  • Mysql注入導圖-學習篇
    每次以為自己都弄懂了之後都會有新的東西冒出來,需要再次學習,一路走來效率不高,工作量很大。而且隨著知識體系的壯大,很多東西會漸漸忘記。因此萌生了寫一個思維導圖的想法,一來整理自己的思路,防止遺忘。二來,作為一名大二的小學長,希望學弟學妹們在這方面能夠學得更快一些。
  • MySQL那些與日期和時間相關的函數
    NOW、CURRENT_TIMESTAMP和SYSDATE  這些函數都能返回當前的系統時間,它們之間有區別嗎?先來看個例子。  SYSDATE函數返回的是執行到當前函數時的時間,而NOW返回的是執行SQL語句時的時間。  因此在上面的例子中,兩次執行SYSDATE函數返回不同的時間是因為第二次調用執行該函數時等待了前面SLEEP函數2秒。而對於NOW函數,不管是在SLEEP函數之前還是之後執行,返回的都是執行這條SQL語句時的時間。
  • 從Web查詢資料庫之PHP與MySQL篇
    下面從Web資料庫架構的工作原理講起。從Web查詢資料庫:Web資料庫架構的工作原理 一個用戶的瀏覽器發出一個HTTP請求,請求特定的Web頁面,在該頁面中出發form表單提交到php腳本文件(如:results.php)中處理 Web伺服器接收到對results.php頁面的請求後,檢索文件,並將其傳遞給PHP引擎處理 PHP引擎開始解析腳本
  • 跟面試官侃半小時MySQL事務隔離性,從基本概念深入到實現
    所謂的讀寫影響注意分為三種:髒讀:讀到了別的事務尚未提交(commit)的變更,別人沒提交,我讀到了。不可重複讀:別的事務提交了變更,被當前事務讀到了。然後導致本事務多次select的結果不一樣,讀到了別的事務提交的內容。幻讀:也是讀到了別的事務提交的內容,但是跟上面的不同之處在於,讀到了原本不存在的記錄。注意,不可重複讀,主要是讀到了別的事務update的內容。
  • MySQL EXPLAIN 命令詳解學習
    一個表可能和一個物理模式表或者在SQL 執行時生成的內部臨時表(例如從子查詢或者合併操作會產生內部臨時表)相關聯。可以參考MySQL Reference Manual 獲得更多信息:http://dev.mysql.com/doc/refman/5.5/en/explain-output.html。
  • 重新學習Mysql資料庫1:無廢話MySQL入門
    本文轉自網際網路本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡查看https://github.com/h2pl/Java-Tutorial喜歡的話麻煩點下Star哈文章也將同步到我的個人博客:www.how2playlife.com本文是微信公眾號【Java技術江湖】的《重新學習
  • 幾個常見而嚴重的 MySQL 問題分析 | 運維進階
    2 原理詳細分析2.1 什麼是MDL鎖?為了在並發環境下維護表元數據的數據一致性,在表上有活動事務(顯式或隱式)的時候,不可以對元數據進行寫入操作。因此從MySQL5.5版本開始引入了MDL鎖(metadata lock),來保護表的元數據信息,用於解決或者保證DDL操作與DML操作之間的一致性。
  • 解析XtraBackup備份MySQL的原理和過程
    就和MySQL在啟動Innodb的時候一樣,會通過比較數據文件頭和redo log文件頭信息來檢查數據是否是一致的,如果不一致就嘗試通過前滾(把redo log中所有提交的事務寫入數據文件)和回滾(從數據文件中撤銷所有redo log中未提交的事務引起的修改)來使數據達到最終一致。
  • MySQL為什麼可以解決髒讀和不可重複讀?
    一般是通過鎖機制,解決掉不可重複讀和幻讀的問題。是不是可以通過樂觀鎖的問題去解決不可重複讀和幻讀的問題,MySQL 採用的是 MVCC 機制來解決髒讀、不可重複讀的問題。MVCC 英文全稱是 Muitiversion Concurrency Control,多版本並發控制技術,原理是通過數據行的多個版本管理實現資料庫的並發控制,通過保存數據的歷史版本,可以通過比較版本號決定數據是否顯示,讀取數據的時候不需要加鎖保證事務的隔離效果。MVCC 是如何解決髒讀的?