詳解 MySQL 的 undo log

大家好,撿田螺的小男孩

今天這篇文章給大家帶來 MySQL 中另外一個重要的日誌 - undo log

文章導讀

undo log 文章導讀

概念

undo log是 innodb 引擎的一種日誌,在事務的修改記錄之前,會把該記錄的原值(before image)先保存起來(undo log)再做修改,以便修改過程中出錯能夠恢復原值或者其他的事務讀取

作用

從概念的定義不難看出undo log的兩個作用:

  1. 事務回滾 - 原子性: undo log 是爲了實現事務的原子性而出現的產物,事務處理的過程中,如果出現了錯誤或者用戶執行ROLLBACK語句,MySQL 可以利用 undo log 中的備份將數據恢復到事務開始之前的狀態。

  2. 多個行版本控制(MVCC)- 隔離性: undo log 在 MySQL InnoDB 儲存引擎中用來實現多版本併發控制,事務未提交之前,當讀取的某一行被其他事務鎖定時,它可以從 undo log 中分析出該行記錄以前的數據是什麼,從而提供該行版本信息,讓用戶實現非鎖定一致性讀取。

什麼時候會生成 undo log

在事務中,進行以下四種操作,都會創建undo log

  1. insert用戶定義的表

  2. update或者delete用戶定義的表

  3. insert用戶定義的臨時表

  4. update或者delete用戶定義的臨時表

存放在哪裏?

既然是一種日誌,儲存在什麼目錄? 又是怎樣儲存的?

儲存在什麼目錄?

這裏要需要說明一下,在MySQL5.6.3之前的版本中,這個undo tablespace是和system tablespace系統表空間存放在一起的,也就是沒有單獨的undo log文件,直接存放在ibdata1文件裏邊,在MySQL5.6.3之後的版本中,MySQL 支持將 undo log tablespace 單獨剝離出來,但這個特性依然很雞肋:

  1. 要在安裝數據庫的時候,就指定好獨立 undo tablespace,在安裝完成後不可更改;

  2. undo tablespace 的 space id 必須從 1 開始,無法增加或者刪除 undo tablespace;

特意安裝了MySQL5.6.39驗證一波:

undo tablespace 表空間設置

到了MySQL5.7版本,終於引入期待已久的功能:即在線 truncate undo tablespace(解決了第一個雞肋點,可以在安裝數據庫之後更改 undo tablespace)

MySQL8.0中,InnoDB 再進一步,對 undo log 做了進一步的改進:

  1. 從 8.0.3 版本開始,默認 undo tablespace 的個數從 0 調整爲 2,也就是在 8.0 版本中,獨立 undo tablespace 被默認打開。修改該參數爲 0 會報 warning 並在未來不再支持;

  2. 無需從 space_id 1 開始創建 undo tablespace,這樣解決了 In-place upgrade 或者物理恢復到一個打開了 Undo tablespace 的實例所產生的 space id 衝突。不過依然要求 undo tablespace 的 space id 是連續分配的;

根據官方的 MySQL 結構圖,我畫了 MySQL 的結構簡圖,描述了 undo log 在數據庫磁盤中的位置,只需要關注簡圖中畫紅色方框綠色方框的模塊。

MySQL 的結構簡圖

我們會發現,隨着 MySQL 版本的迭代,已經把 undo log 單獨剝離出來了,那我們思考一下:爲什麼要支持把 undolog 的 tablespace 單獨剝離出來呢?

這是從性能的角度來考量的。原先的 undolog 和系統表空間共享一個表空間,這樣在記錄 undolog 的時候,和其他的一些使用系統表空間來存儲的操作肯定會存在磁盤 I/O 的競爭。但是如果我們把 undolog 的表空間單獨拉出來,支持讓其自定義目錄和表空間的數量,這樣我們可以把 undolog 配置單獨的磁盤目錄,提高 undo log 日誌的讀寫性能,也能方便 DBA 操作。

閱讀到這裏,我們弄清楚了 undo log 是儲存在單獨的 undo tablespace,接下來我們繼續研究 undo tablespace 是以什麼樣的結構儲存日誌內容的。

undo tablespace - 表空間

在 MySQL 中,undo tablespace 定義了回滾段 rollback segments 用來存放 undo log。

我們這裏來看一下 undo tablespace 的結構體源碼。

(ps:我們還是要養成看源碼的習慣,我們搜索到的知識觀點很多,如何甄別觀點的對與錯,只有從源碼層面找到答案,當然這裏看 MySQL 源碼只是爲了進一步說明 undo tablespace 表空間定義了多個 rollback segments - rseg)

unbo tablespace 表空間結構體源碼路徑

mysql-server-mysql-8.0.13/storage/innobase/include/trx0purge.h

undo tablespace 結構體定義

/** An undo::Tablespace object is used to easily convert between
undo_space_id and undo_space_num and to create the automatic file_name
and space name.  In addition, it is used in undo::Tablespaces to track
the trx_rseg_t objects in an Rsegs vector. So we do not allocate the
Rsegs vector for each object, only when requested by the constructor. */
struct Tablespace {
 /** ... **/
 private:
  /** Undo Tablespace ID. */
  space_id_t m_id;

  /** Undo Tablespace number, from 1 to 127. This is the
  7-bit number that is used in a rollback pointer.
  Use id2num() to get this number from a space_id. */
  space_id_t m_num;

  /** The tablespace name, auto-generated when needed from
  the space number. */
  char *m_space_name;

  /** The tablespace file name, auto-generated when needed
  from the space number. */
  char *m_file_name;

  /** The tablespace log file name, auto-generated when needed
  from the space number. */
  char *m_log_file_name;

  /** List of rollback segments within this tablespace.
  This is not always used. Must call init_rsegs to use it. */
  Rsegs *m_rsegs;
};

從上邊的源碼可知,在我們的 undo tablespace 表空間結構體定義裏邊,有Rsegs的定義,這個就是我們前邊說的回滾段(Rollback Segments),我們繼續從源碼來研究回滾段(Rollback Segments)結構體。

resg - 回滾段

回滾段結構體源碼路徑

mysql-server-mysql-8.0.13/storage/innobase/include/trx0types.h

回滾段 rseg 結構體源碼

undo log tablespace 結構體中Rsegstrx_rseg_tstd::vector封裝

/** The rollback segment memory object */
struct trx_rseg_t {
  /*--------------------------------------------------------*/
  /** rollback segment id == the index of its slot in the trx
  system file copy */
  ulint id;

  /** mutex protecting the fields in this struct except id,space,page_no
  which are constant */
  RsegMutex mutex;

  /** space ID where the rollback segment header is placed */
  space_id_t space_id;

  /** page number of the rollback segment header */
  page_no_t page_no;

  /** page size of the relevant tablespace */
  page_size_t page_size;

  /** maximum allowed size in pages */
  ulint max_size;

  /** current size in pages */
  ulint curr_size;

  /*--------------------------------------------------------*/
  /* Fields for update undo logs */
  /** List of update undo logs */
  UT_LIST_BASE_NODE_T(trx_undo_t) update_undo_list;

  /** List of update undo log segments cached for fast reuse */
  UT_LIST_BASE_NODE_T(trx_undo_t) update_undo_cached;

  /*--------------------------------------------------------*/
  /* Fields for insert undo logs */
  /** List of insert undo logs */
  UT_LIST_BASE_NODE_T(trx_undo_t) insert_undo_list;

  /** List of insert undo log segments cached for fast reuse */
  UT_LIST_BASE_NODE_T(trx_undo_t) insert_undo_cached;

  /*--------------------------------------------------------*/

  /** Page number of the last not yet purged log header in the history
  list; FIL_NULL if all list purged */
  page_no_t last_page_no;

  /** Byte offset of the last not yet purged log header */
  ulint last_offset;

  /** Transaction number of the last not yet purged log */
  trx_id_t last_trx_no;

  /** TRUE if the last not yet purged log needs purging */
  ibool last_del_marks;

  /** Reference counter to track rseg allocated transactions. */
  std::atomic<ulint> trx_ref_count;
};

每個回滾段維護了一個Rollback Segment Header Page,限於篇幅,這裏不再深入研究,因爲他不影響我們繼續閱讀,如果感興趣的讀者,可以看我最後貼出來的鏈接深入瞭解。

undo tablespace 儲存結構示意圖

爲了鞏固前邊說的內容,這裏我畫了一張 undo tablespace 表空間結構圖,希望能幫您鞏固。

undo tablespace 表空間結構圖

undo log 的類型

爲了更好的處理回滾,undo log 和之前說的 redo log 記錄物理日誌不一樣,它是邏輯日誌,可以認爲當 delete 一條記錄時,undo log 中會記錄一條對應的 insert 記錄,反之亦然,當 update 一條記錄時,它記錄一條對應相反的 update 記錄。 對應着 undo log 的兩種類型,分別是 insert undo logupdate undo log

insert undo log 長啥樣

對於 insert 類型的 sql,會在 undo log 中記錄下方纔你 insert 進來的數據的 ID,根據 ID 完成精準的刪除。

insert 類型的 undo log 長下面這樣:

insert undo log - 不是我畫的

可能你打眼一看上圖就能知道各部分都有啥用。但是,不知道你會不會納悶這樣一個問題:不是說對於 insert 類型的 undo log MySQL 記錄的是方纔插入行 ID 嗎?怎麼上圖整出來的了這麼多 Col1、Col2、Col2。其實是 MySQL 設計的很周到,因爲它是針對聯合主鍵設計的。

update undo log 長啥樣

一條 update sql 對應 undolog 長如下這樣:

update undo log - 不是我畫的

通過上邊的基礎鋪墊,來到我們的實戰分析環節。

場景實戰

事務怎麼回滾的?

舉一個舉例的案例來說明該過程。

insert 類型的 undo log

對於 insert 類型的 sql,會在 undo log 中記錄下 insert 進來的數據的 ID,當你想 roll back 時,根據 ID 完成精準的刪除。對於 delete 類型的 sql,會在 undo log 中記錄方纔你刪除的數據,當你回滾時會將刪除前的數據 insert 進去。對於 update 類型的 sql,會在 undo log 中記錄下修改前的數據,回滾時只需要反向 update 即可。對於 select 類型的 sql,別費心了,select 不需要回滾。先看一個簡單的 insert undo log 鏈條

insert undo log 鏈條 - 不是我畫的

有一個注意點:因爲單純的 insert sql 不涉及多 MVCC 的能力。所以一旦事務 commit,這條 insert undo log 就可以直接刪除了。

update 類型的 undo log

爲了方便畫圖,重點突出鏈條的概念我省略了 update undo log 的部分內容 一個事物 A 開啓後插圖了一條記錄:name = tom,MySQL 會記錄下這樣一條 undo log

undo log 記錄 - 不是我畫的

隨後先後來了兩個事物:事物 B,事物 ID=61,它執行 sql 將 name 改成 jerry。事物 C,事物 ID=62,它執行 sql 將 name 改成 tom。於是 MySQL 記錄下這樣一條新的 undo log

事務執行邏輯 - 不是我畫的

你可以看到,MySQL 會將對一行數據的修改 undo log 通過 DATA_ROLL_ID 指針連接在一起形成一個 undo log 鏈表鏈條。這樣事物 C 如果想回滾,他會將數據回滾到事物 B 修改後的狀態。而事物 B 想回滾他會將數據回滾到事物 A 的狀態。

淺談 MVCC 工作原理

undo log 在事務開啓之前就產生,當事務提交的時候,不會刪除 undo log,因爲可能需要 rollback 操作,要執行回滾(rollback)操作時,從緩存中讀取數據。InnoDB 會將事務對應的日誌保存在刪除 list 中,後臺通過 purge 線程進行回收處理。

還是以一條 sql 執行 update、select 過程來淺析 MVCC 的工作原理:

執行 update 操作,事務 A 提交時候(事務還沒提交),會將數據進行備份,備份到對應的 undo buffer,undo log 保存了未提交之前的操作日誌,User 表數據肯定就是持久保存到 InnoDB 的數據文件 IBD,默認情況。

這時事務 B 進行查詢操作,是直接讀 undo buffer 緩存的,這時事務 A 還沒提交事務,要回滾(rollback),是不讀磁盤的,先直接從 undo buffer 緩存讀取。

淺析 MVCC 工作原理 - 不是我畫的

總結

這篇文章到這裏就寫完了,從undo log概念出發,依次介紹了生成 undo log、存放在哪裏並且以什麼方式儲存的,最後結合場景實戰分析了undo log的變化過程。

  關注螺哥面試的公衆號,看硬核面試題

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xZmZmqTcERGmaNu923EcLw