MySQL 事務的底層原理和 MVCC(二)

7.2. undo 日志

7.2.1. 事務回滾的需求

我們說過事務需要保證原子性,也就是事務中的操作要么全部完成,要么什么也不做。但是偏偏有時候事務執行到一半會出現一些情況,比如:
情況一:事務執行過程中可能遇到各種錯誤,比如服務器本身的錯誤,操作系統錯誤,甚至是突然斷電導致的錯誤。
情況二:程序員可以在事務執行過程中手動輸入 ROLLBACK 語句結束當前的事務的執行。

這兩種情況都會導致事務執行到一半就結束,但是事務執行過程中可能已經修改了很多東西,為了保證事務的原子性,我們需要把東西改回原先的樣子,這個過程就稱之為回滾(英文名:rollback),這樣就可以造成這個事務看起來什么都沒做,所以符合原子性要求。

每當我們要對一條記錄做改動時(這里的改動可以指 INSERT、DELETE、UPDATE),都需要把回滾時所需的東西都給記下來。比方說:
你插入一條記錄時,至少要把這條記錄的主鍵值記下來,之后回滾的時候只需要把這個主鍵值對應的記錄刪掉。
你刪除了一條記錄,至少要把這條記錄中的內容都記下來,這樣之后回滾時再把由這些內容組成的記錄插入到表中。
你修改了一條記錄,至少要把修改這條記錄前的舊值都記錄下來,這樣之后回滾時再把這條記錄更新為舊值。
這些為了回滾而記錄的這些東西稱之為撤銷日志,英文名為 undo log/undo日志。這里需要注意的一點是,由于查詢操作(SELECT)并不會修改任何用戶記錄,所以在查詢操作執行時,并不需要記錄相應的 undo 日志。

當然,在真實的 InnoDB 中,undo 日志其實并不像我們上邊所說的那么簡單,不同類型的操作產生的 undo 日志的格式也是不同的。

7.2.2. 事務 id

7.2.2.1. 給事務分配 id 的時機

一個事務可以是一個只讀事務,或者是一個讀寫事務:
我們可以通過 START TRANSACTION READ ONLY 語句開啟一個只讀事務。
在只讀事務中不可以對普通的表(其他事務也能訪問到的表)進行增、刪、改操作,但可以對用戶臨時表做增、刪、改操作。
我們可以通過 START TRANSACTION READ WRITE 語句開啟一個讀寫事務,或者使用 BEGIN、START TRANSACTION 語句開啟的事務默認也算是讀寫事務。
在讀寫事務中可以對表執行增刪改查操作。
如果某個事務執行過程中對某個表執行了增、刪、改操作,那么 InnoDB 存儲引擎就會給它分配一個獨一無二的事務 id,分配方式如下:
對于只讀事務來說,只有在它第一次對某個用戶創建的臨時表執行增、刪、改操作時才會為這個事務分配一個事務 id,否則的話是不分配事務 id 的。
我們前邊說過對某個查詢語句執行 EXPLAIN 分析它的查詢計劃時,有時候在Extra 列會看到 Using temporary 的提示,這個表明在執行該查詢語句時會用到內部臨時表。這個所謂的內部臨時表和我們手動用 CREATE TEMPORARY TABLE 創建的用戶臨時表并不一樣,在事務回滾時并不需要把執行 SELECT 語句過程中用到的內部臨時表也回滾,在執行 SELECT 語句用到內部臨時表時并不會為它分配事務 id。
對于讀寫事務來說,只有在它第一次對某個表(包括用戶創建的臨時表)執行增、刪、改操作時才會為這個事務分配一個事務 id,否則的話也是不分配事務id 的。
有的時候雖然我們開啟了一個讀寫事務,但是在這個事務中全是查詢語句,并沒有執行增、刪、改的語句,那也就意味著這個事務并不會被分配一個事務 id。
上邊描述的事務 id 分配策略是針對 MySQL 5.7 來說的,前邊的版本的分配方式可能不同。

7.2.2.2. 事務 id 生成機制

這個事務 id 本質上就是一個數字,它的分配策略和我們前邊提到的對隱藏列 row_id(當用戶沒有為表創建主鍵和 UNIQUE 鍵時 InnoDB 自動創建的列)的分配策略大抵相同,具體策略如下:

服務器會在內存中維護一個全局變量,每當需要為某個事務分配一個事務 id時,就會把該變量的值當作事務 id 分配給該事務,并且把該變量自增 1。

每當這個變量的值為 256 的倍數時,就會將該變量的值刷新到系統表空間的頁號為 5 的頁面中一個稱之為 Max Trx ID 的屬性處,這個屬性占用 8 個字節的存儲空間。
當系統下一次重新啟動時,會將上邊提到的 Max Trx ID 屬性加載到內存中,將該值加上 256 之后賦值給我們前邊提到的全局變量(因為在上次關機時該全局變量的值可能大于 Max Trx ID 屬性值)。
這樣就可以保證整個系統中分配的事務 id 值是一個遞增的數字。先被分配id 的事務得到的是較小的事務 id,后被分配 id 的事務得到的是較大的事務 id。

7.2.3. trx_id 隱藏列

我們在學習 InnoDB 記錄行格式的時候重點強調過:聚簇索引的記錄除了會保存完整的用戶數據以外,而且還會自動添加名為 trx_id、roll_pointer 的隱藏列,
如果用戶沒有在表中定義主鍵以及 UNIQUE 鍵,還會自動添加一個名為 row_id的隱藏列。
在這里插入圖片描述
其中的 trx_id 列就是某個對這個聚簇索引記錄做改動的語句所在的事務對應的事務 id 而已(此處的改動可以是 INSERT、DELETE、UPDATE 操作)。至于roll_pointer 隱藏列我們后邊分析。

7.2.4. undo 日志的格式

為了實現事務的原子性,InnoDB 存儲引擎在實際進行增、刪、改一條記錄時,都需要先把對應的 undo 日志記下來。一般每對一條記錄做一次改動,就對應著一條 undo 日志,但在某些更新記錄的操作中,也可能會對應著 2 條 undo日志。
一個事務在執行過程中可能新增、刪除、更新若干條記錄,也就是說需要記錄很多條對應的 undo 日志,這些 undo 日志會被從 0 開始編號,也就是說根據生成的順序分別被稱為第 0 號 undo 日志、第 1 號 undo 日志、…、第 n 號 undo日志等,這個編號也被稱之為 undo no。
這些 undo 日志是被記錄到類型為 FIL_PAGE_UNDO_LOG 的頁面中。這些頁面可以從系統表空間中分配,也可以從一種專門存放 undo 日志的表空間,也就是所謂的 undo tablespace 中分配。先來看看不同操作都會產生什么樣子的 undo日志。

7.2.4.1. INSERT 操作對應的 undo 日志

當我們向表中插入一條記錄時最終導致的結果就是這條記錄被放到了一個
數據頁中。如果希望回滾這個插入操作,那么把這條記錄刪除就好了,也就是說在寫對應的 undo 日志時,主要是把這條記錄的主鍵信息記上。InnoDB 的設計了一個類型為 TRX_UNDO_INSERT_REC 的 undo 日志。

如果記錄中的主鍵只包含一個列,那么在類型為 TRX_UNDO_INSERT_REC 的undo 日志中只需要把該列占用的存儲空間大小和真實值記錄下來,如果記錄中的主鍵包含多個列,那么每個列占用的存儲空間大小和對應的真實值都需要記錄下來。
當我們向某個表中插入一條記錄時,實際上需要向聚簇索引和所有的二級索引都插入一條記錄。不過記錄 undo 日志時,我們只需要考慮向聚簇索引插入記錄時的情況就好了,因為其實聚簇索引記錄和二級索引記錄是一一對應的,我們在回滾插入操作時,只需要知道這條記錄的主鍵信息,然后根據主鍵信息做對應的刪除操作,做刪除操作時就會順帶著把所有二級索引中相應的記錄也刪除掉。
后邊說到的 DELETE 操作和 UPDATE 操作對應的 undo 日志也都是針對聚簇索引記錄而言的。

roll_pointer 的作用
roll_pointer 本質上就是一個指向記錄對應的 undo 日志的一個指針。比方說我們向表里插入了 2 條記錄,每條記錄都有與其對應的一條 undo 日志。記錄被存儲到了類型為 FIL_PAGE_INDEX 的頁面中(就是我們前邊一直所說的數據頁),undo 日志被存放到了類型為FIL_PAGE_UNDO_LOG 的頁面中。roll_pointer 本質就是一個指針,指向記錄對應的 undo 日志。

7.2.4.2. DELETE 操作對應的 undo 日志

我們知道插入到頁面中的記錄會根據記錄頭信息中的 next_record 屬性組成一個單向鏈表,我們把這個鏈表稱之為正常記錄鏈表;被刪除的記錄其實也會根據記錄頭信息中的 next_record 屬性組成一個鏈表,只不過這個鏈表中的記錄占用的存儲空間可以被重新利用,所以也稱這個鏈表為垃圾鏈表。Page Header 部分有一個稱之為 PAGE_FREE 的屬性,它指向由被刪除記錄組成的垃圾鏈表中的頭節點。
假設此刻某個頁面中的記錄分布情況是這樣的
在這里插入圖片描述
我們只把記錄的 delete_mask 標志位展示了出來。從圖中可以看出,正常記錄鏈表中包含了 3 條正常記錄,垃圾鏈表里包含了 2 條已刪除記錄。頁面的 PageHeader 部分的 PAGE_FREE 屬性的值代表指向垃圾鏈表頭節點的指針。
假設現在我們準備使用 DELETE 語句把正常記錄鏈表中的最后一條記錄給刪除掉,其實這個刪除的過程需要經歷兩個階段:

階段一:將記錄的delete_mask標識位設置為1,這個階段稱之為delete mark。
在這里插入圖片描述
可以看到,正常記錄鏈表中的最后一條記錄的 delete_mask 值被設置為 1,但是并沒有被加入到垃圾鏈表。也就是此時記錄處于一個中間狀態。在刪除語句所在的事務提交之前,被刪除的記錄一直都處于這種所謂的中間狀態。
為啥會有這種奇怪的中間狀態呢?其實主要是為了實現一個稱之為 MVCC的功能,稍后再介紹。

階段二:當該刪除語句所在的事務提交之后,會有專門的線程后來真正的把記錄刪除掉。所謂真正的刪除就是把該記錄從正常記錄鏈表中移除,并且加入到垃圾鏈表中,然后還要調整一些頁面的其他信息,比如頁面中的用戶記錄數量PAGE_N_RECS、上次插入記錄的位置PAGE_LAST_INSERT、垃圾鏈表頭節點的指針 PAGE_FREE、頁面中可重用的字節數量 PAGE_GARBAGE、還有頁目錄的一些信息等等。這個階段稱之為 purge。
把階段二執行完了,這條記錄就算是真正的被刪除掉了。這條已刪除記錄占用的存儲空間也可以被重新利用了。
在這里插入圖片描述
從上邊的描述中我們也可以看出來,在刪除語句所在的事務提交之前,只會經歷階段一,也就是 delete mark 階段(提交之后我們就不用回滾了,所以只需考慮對刪除操作的階段一做的影響進行回滾)。InnoDB 中就會產生一種稱之為TRX_UNDO_DEL_MARK_REC 類型的 undo 日志。

版本鏈
同時,在對一條記錄進行 delete mark 操作前,需要把該記錄的舊的 trx_id和 roll_pointer 隱藏列的值都給記到對應的 undo 日志中來,就是我們圖中顯示的old trx_id 和 old roll_pointer 屬性。這樣有一個好處,那就是可以通過 undo 日志的 old roll_pointer 找到記錄在修改之前對應的 undo 日志。比方說在一個事務中,我們先插入了一條記錄,然后又執行對該記錄的刪除操作,這個過程的示意圖就是這樣:
在這里插入圖片描述
從圖中可以看出來,執行完delete mark操作后,它對應的undo日志和INSERT操作對應的 undo 日志就串成了一個鏈表。這個鏈表就稱之為版本鏈。

7.2.4.3. UPDATE 操作對應的 undo 日志

在執行 UPDATE 語句時,InnoDB 對更新主鍵和不更新主鍵這兩種情況有截然不同的處理方案。
不更新主鍵的情況
在不更新主鍵的情況下,又可以細分為被更新的列占用的存儲空間不發生變化和發生變化的情況。
就地更新(in-place update)
更新記錄時,對于被更新的每個列來說,如果更新后的列和更新前的列占用的存儲空間都一樣大,那么就可以進行就地更新,也就是直接在原記錄的基礎上修改對應列的值。再次強調一邊,是每個列在更新前后占用的存儲空間一樣大,有任何一個被更新的列更新前比更新后占用的存儲空間大,或者更新前比更新后占用的存儲空間小都不能進行就地更新。
先刪除掉舊記錄,再插入新記錄
在不更新主鍵的情況下,如果有任何一個被更新的列更新前和更新后占用的存儲空間大小不一致,那么就需要先把這條舊的記錄從聚簇索引頁面中刪除掉,然后再根據更新后列的值創建一條新的記錄插入到頁面中。
請注意一下,我們這里所說的刪除并不是 delete mark 操作,而是真正的刪除掉,也就是把這條記錄從正常記錄鏈表中移除并加入到垃圾鏈表中,并且修改頁面中相應的統計信息(比如 PAGE_FREE、PAGE_GARBAGE 等這些信息)。由用戶線程同步執行真正的刪除操作,真正刪除之后緊接著就要根據各個列更新后的值創建的新記錄插入。
這里如果新創建的記錄占用的存儲空間大小不超過舊記錄占用的空間,那么可以直接重用被加入到垃圾鏈表中的舊記錄所占用的存儲空間,否則的話需要在頁面中新申請一段空間以供新記錄使用,如果本頁面內已經沒有可用的空間的話,那就需要進行頁面分裂操作,然后再插入新記錄。
針對 UPDATE 不更新主鍵的情況(包括上邊所說的就地更新和先刪除舊記錄再插入新記錄),InnoDB 設計了一種類型為TRX_UNDO_UPD_EXIST_REC 的 undo日志。

更新主鍵的情況

在聚簇索引中,記錄是按照主鍵值的大小連成了一個單向鏈表的,如果我們更新了某條記錄的主鍵值,意味著這條記錄在聚簇索引中的位置將會發生改變,比如你將記錄的主鍵值從 1 更新為 10000,如果還有非常多的記錄的主鍵值分布在 1 ~ 10000 之間的話,那么這兩條記錄在聚簇索引中就有可能離得非常遠,甚至中間隔了好多個頁面。針對 UPDATE 語句中更新了記錄主鍵值的這種情況,InnoDB 在聚簇索引中分了兩步處理:

將舊記錄進行 delete mark 操作
也就是說在 UPDATE 語句所在的事務提交前,對舊記錄只做一個 delete mark操作,在事務提交后才由專門的線程做 purge 操作,把它加入到垃圾鏈表中。這里一定要和我們上邊所說的在不更新記錄主鍵值時,先真正刪除舊記錄,再插入新記錄的方式區分開!
之所以只對舊記錄做 delete mark 操作,是因為別的事務同時也可能訪問這條記錄,如果把它真正的刪除加入到垃圾鏈表后,別的事務就訪問不到了。這個功能就是所謂的 MVCC。
創建一條新記錄
根據更新后各列的值創建一條新記錄,并將其插入到聚簇索引中(需重新定位插入的位置)。
由于更新后的記錄主鍵值發生了改變,所以需要重新從聚簇索引中定位這條記錄所在的位置,然后把它插進去。
針對 UPDATE 語句更新記錄主鍵值的這種情況,在對該記錄進行 delete mark操作前,會記錄一條類型為 TRX_UNDO_DEL_MARK_REC 的 undo 日志;之后插入新記錄時,會記錄一條類型為 TRX_UNDO_INSERT_REC 的 undo 日志,也就是說每對一條記錄的主鍵值做改動時,會記錄 2 條 undo 日志。

7.2.5. FIL_PAGE_UNDO_LOG 頁面

我們前邊說明表空間的時候說過,表空間其實是由許許多多的頁面構成的,頁面默認大小為 16KB。這些頁面有不同的類型,比如類型為 FIL_PAGE_INDEX 的頁面用于存儲聚簇索引以及二級索引,類型為 FIL_PAGE_TYPE_FSP_HDR 的頁面用于存儲表空間頭部信息的,還有其他各種類型的頁面,其中有一種稱之為FIL_PAGE_UNDO_LOG 類型的頁面是專門用來存儲 undo 日志的。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/160388.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/160388.shtml
英文地址,請注明出處:http://en.pswp.cn/news/160388.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

ORB-SLAM3在windows11下的編譯使用

01 寫在前面 近期在學習SLAM,想部署一下ORB-SLAM3,但是自己電腦是win11系統,因此就想著在win11上部署一下。但是網上看了一些教程,有一些博客,但是可能不適合我這種情況把,就很糾結。先說下結果&#xff0…

【python基礎(三)】操作列表:for循環、正確縮進、切片的使用、元組

文章目錄 一. 遍歷整個列表1. 在for循環中執行更多操作2. 在for循環結束后執行一些操作 二. 避免縮進錯誤三. 創建數值列表1. 使用函數range()2. 使用range()創建數字列表3. 指定步長。4. 對數字列表執行簡單的統計計算5. 列表解析 五. 使用列表的一部分-切片1. 切片2. 遍歷切片…

【并發編程】ThreadLocal詳解與原理

📫作者簡介:小明Java問道之路,2022年度博客之星全國TOP3,專注于后端、中間件、計算機底層、架構設計演進與穩定性建設優化,文章內容兼具廣度、深度、大廠技術方案,對待技術喜歡推理加驗證,就職于…

【電路筆記】-電流源

電流源 文章目錄 電流源1、概述1.1 理想電流源1.2 實際電流源1.3 連接規則 2、依賴電流2.1 壓控電流源2.2 電流控制電流源 3、總結 本文為前面文章 電壓源的延續,我們將在本文介紹電流源。 與電壓源的情況類似,我們將首先介紹理想電流源的概念&#xff…

MySQL 8.2 Command Line Client打開時一閃而過閃退問題

MySQL8.2安裝成功后,發現打開MySQL 8.0 Command Line Client時出現一閃而過,打不開的情況。 解決方案: 1、打開MySQL 8.2 Command Line Client文件位置 2、右鍵選擇屬性 3、復制它的目標 4、我復制下來的目標路徑是這樣的,"…

關于 Docker

關于 Docker 1. 術語Docker Enginedockerd(Docker daemon)containerdOCI (Open Container Initiative)runcDocker shimCRI (Container Runtime Interface)CRI-O 2. 容器啟動過程在 Linux 中的實現daemon 的作用 Docker 是個劃時代的開源項目,…

[計算機網絡實驗]頭歌 實驗二 以太網幀、IP報文分析(含部分分析)

目錄 第1關:Wireshark基本使用入門 【實驗目的】 【實驗環境】 【本地主機、平臺虛擬機之間數據傳遞】 wireshark基本用法】 1、wireshark主界面 2、抓取分組操作 3、Wireshark窗口功能 4、篩選分組操作 【實驗操作】 ?編輯 第2關:Ethernet幀…

編程語言發展史:C++語言的發展和應用

預計更新 第一部分:早期編程語言 1.1布爾代數和機器語言 1.2匯編語言的出現和發展 1.3高級語言的興起 第二部分:主流編程語言 1.1 C語言的誕生及其影響 1.2 C語言的發展和應用 1.3 Java語言的出現和發展 1.4 Python語言的興起和特點 1.5 JavaScript語言…

基于Towers of Binary Fields的succinct arguments

1. 引言 Ulvetanna團隊Benjamin E. Diamond和Jim Posen 2023年論文《Succinct Arguments over Towers of Binary Fields》,開源代碼見: https://github.com/recmo/binius(Rust Sage)【基于plonky3等庫】 在該論文中&#xff1…

Apache POI簡介

三十二、Apache POI 32.1 介紹 Apache POI 是一個處理Miscrosoft Office各種文件格式的開源項目。簡單來說就是,我們可以使用POI在Java程序中對Miscrosoft Office各種文件進行讀寫操作。 一般情況下,POI都是用于操作Excel文件。 Apache POI 的應用場…

基于區域劃分的GaN HEMT 準物理大信號模型

GaN HEMT器件的大信號等效電路模型分為經驗基模型和物理基模型。經驗基模型具有較高精度但參數提取困難,特別在GaN HEMT器件工藝不穩定的情況下不易應用。相比之下,物理基模型從器件工作機理出發,參數提取相對方便,且更容易更新和…

火山引擎 ByteHouse 的增強型數據導入技術實踐

作為企業數字化建設的必備要素,易用的數據引擎能幫助企業提升數據使用效率,更好提升數據應用價值,夯實數字化建設基礎。 數據導入是衡量OLAP引擎性能及易用性的重要標準之一,高效的數據導入能力能夠加速數據實時處理和分析的效率。…

Sa-Token 整合Java17和SpringBoot

目錄 前言引入項目開啟登錄認證路由攔截鑒權解決兼容問題總結 前言 之前無意中發現Sa-Token權限認證框架,項目十分好用。 項目地址: https://github.com/dromara/sa-token 官網地址: https://sa-token.cc/doc.html#/start/example 我的個人…

如何輕松應對企業網絡管理挑戰,釋放網絡靈活性

企業在日常經營中,越來越依賴于云應用程序,分散的團隊和統一通信。這些變化使得保持網絡連接性不僅是必要的,而且對任務的成功完成至關重要。 傳統的廣域網(WAN)并不總能適應這些挑戰,因為它們往往無法提供…

不停的挖掘硬盤的最大潛能

從 NAS 上退休的硬盤被用在了監控的存儲上了。 隨著硬盤使用壽命的接近尾聲,感覺就是從高附加值數據到低附加值數據上。監控數據只會保留那么幾個月的時間,很多時候都會被覆蓋重新寫入。 有人問為什么監控數據不保留幾年的,那是因為監控數據…

java_函數式接口

文章目錄 一、什么是函數式接口二、四大核心函數式接口三、使用舉例 一、什么是函數式接口 如果一個接口只有一個抽象方法,那么該接口就是一個函數式接口函數式接口的實例可以通過 lambda 表達式、方法引用或者構造方法引用來創建如果我們在某個接口上聲明了 Funct…

【Unity入門】鼠標輸入和鍵盤輸入

Unity的Input類提供了許多監聽用戶輸入的方法,比如我們常見的鼠標,鍵盤,手柄等。我們可以用Input類的接口來獲取用戶的輸入信息 一、監聽鼠標輸入 GetMouseButtonUp 、GetMouseButtonDown、GetMouseButton input.GetMouseButtonDown和 inp…

從mysql源碼編譯出相應的庫和可執行文件及搭建mysql服務端

目錄 1. 問題的提出 2. 源碼下載 3. 升級或安裝某些前置軟件 3.1. 升級CMake 3.2. 升級gcc、g 4. 安裝依賴庫 4.1. 安裝OpenSSL 4.2. 安裝Curses 4.3. 安裝pkg-config 5. 編譯、安裝 6. 編譯結果、配置 7. 編譯錯誤處理 7.1. 錯誤1 7.2. 錯誤2 8. 搭建mysql數…

VMware三種網絡模式

橋接模式 NAT(網絡地址轉換模式) Host-Only(僅主機模式) 參考: vmware虛擬機三種網絡模式 - 知乎 (zhihu.com)

【中國平安社招校招】【內推】【當天內推】

中國平安社招校招內推 通過內推鏈接即時內推,反饋速度比正常要快(可私信問進度) 開放大量HC(不限崗位和地區,技術、設計、產品、運營、數據等都可內推) Step1:查看相關職位 瀏覽平安的招聘官網:復制以下地址至瀏覽器打開。注意以下鏈接包含…