一起來分析MySQL事務工作流程原理

2022-06-01 14:00:47
本篇文章給大家帶來了關於的相關知識,其中主要介紹了事務工作流程原理的相關問題,包括了事務的原子性是通過undo log來實現的、事務的永續性是通過redo log來實現的等等內容,下面一起來看一下,希望對大家有幫助。

推薦學習:

  • 事務的原子性是通過 undo log 來實現的
  • 事務的永續性是通過 redo log 來實現的
  • 事務的隔離性是通過 (讀寫鎖+MVCC)來實現的
  • 而事務的終極大 boss 一致性是通過原子性,永續性,隔離性來實現的!!!

1、redo log 實現永續性

問題1: 為什麼需要redo log?

InnoDB作為MySQL的儲存引擎,資料是存放在磁碟中的,但如果每次讀寫資料都需要磁碟IO,效率會很低。為此,InnoDB提供了快取(Buffer Pool),作為存取資料庫的緩衝:當從資料庫讀取資料時,會首先從Buffer Pool中讀取,如果Buffer Pool中沒有,則從磁碟讀取後放入Buffer Pool;當向資料庫寫入資料時,會首先寫入Buffer Pool,Buffer Pool中修改的資料會定期重新整理到磁碟中 。

Buffer Pool的使用大大提高了讀寫資料的效率,但是也帶了新的問題:如果MySQL宕機,而此時Buffer Pool中修改的資料還沒有重新整理到磁碟,就會導致資料的丟失,事務的永續性無法保證。

問題2:redo log如何保證事務的永續性?

Redo log可以簡單分為以下兩個部分:

一是記憶體中重做紀錄檔緩衝 (redo log buffer),是易失的,在記憶體中

二是重做紀錄檔檔案 (redo log file),是持久的,儲存在磁碟中

這裡再細說下寫入Redo的時機:

在資料頁修改完成之後,在髒頁刷出磁碟之前,寫入redo紀錄檔。注意的是先修改資料,後寫紀錄檔

redo紀錄檔比資料頁先寫回磁碟

聚集索引、二級索引、undo頁面的修改,均需要記錄Redo紀錄檔

在 MySQL中,如果每一次的更新操作都需要寫進磁碟,然後磁碟也要找到對應的那條記錄,然後再更新,整個過程 IO 成本、查詢成本都很高。為了解決這個問題,MySQL 的設計者就採用了紀錄檔(redo log)來提升更新效率。

當事務提交時,先將 redo log buffer 寫入到 redo log file 進行持久化,待事務的commit操作完成時才算完成。這種做法也被稱為 Write-Ahead Log(預先紀錄檔持久化),在持久化一個資料頁之前,先將記憶體中相應的紀錄檔頁持久化。

具體來說,當有一條記錄需要更新的時候,InnoDB 引擎就會先把記錄寫到 redo log(redo log buffer)裡面,並更新記憶體(buffer pool),這個時候更新就算完成了。同時,InnoDB 引擎會在適當的時候(如系統空閒時),將這個操作記錄更新到磁碟裡面(刷髒頁)。

在一個事務中可以修改多個頁,Write-Ahead Log 可以保證單個資料頁的一致性,但是無法保證事務的永續性,Force-log-at-commit 要求當一個事務提交時,其產生所有的mini-transaction 紀錄檔必須重新整理到磁碟中,若紀錄檔重新整理完成後,在緩衝池中的頁重新整理到持久化儲存裝置前資料庫發生了宕機,那麼資料庫重新啟動時,可以通過紀錄檔來保證資料的完整性。

問題3:重寫紀錄檔的流程?

上圖表示了重做紀錄檔的寫入流程,每個mini-transaction對應每一條DML操作,比如一條update語句,其由一個mini-transaction來保證,對資料修改後,產生redo1,首先將其寫入mini-transaction私有的Buffer中,update語句結束後,將redo1從私有Buffer拷貝到公有的Log Buffer中。當整個外部事務提交時,將redo log buffer再刷入到redo log file中。( redo log是按照順序寫入的,磁碟的順序讀寫的速度遠大於隨機讀寫)

問題4:資料寫入後的最終落盤,是從 redo log 更新過來的還是從 buffer pool 更新過來的呢?

實際上,redo log 並沒有記錄資料頁的完整資料,所以它並沒有能力自己去更新磁碟資料頁,也就不存在由 redo log 更新過去資料最終落盤的情況。

① 資料頁被修改以後,跟磁碟的資料頁不一致,稱為髒頁。最終資料落盤,就是把記憶體中的資料頁寫盤。這個過程與 redo log 毫無關係。

② 在崩潰恢復場景中,InnoDB 如果判斷到一個資料頁可能在崩潰恢復的時候丟失了更新,就會將它讀到記憶體,然後讓 redo log 更新記憶體內容。更新完成後,記憶體頁變成髒頁,就回到了第一種情況的狀態

問題5:redo log buffer 是什麼?是先修改記憶體,還是先寫 redo log 檔案?

在一個事務的更新過程中,紀錄檔是要寫多次的。比如下面這個事務:

Copybegin;

INSERT INTO T1 VALUES ('1', '1');

INSERT INTO T2 VALUES ('1', '1');

commit;

這個事務要往兩個表中插入記錄,插入資料的過程中,生成的紀錄檔都得先儲存起來,但又不能在還沒 commit 的時候就直接寫到 redo log 檔案裡。

因此就需要 redo log buffer 出場了,它就是一塊記憶體,用來先存 redo 紀錄檔的。也就是說,在執行第一個 insert 的時候,資料的記憶體被修改了,redo log buffer 也寫入了紀錄檔。

但是,真正把紀錄檔寫到 redo log 檔案,是在執行 commit 語句的時候做的。

redo log buffer 本質上只是一個 byte 陣列,但是為了維護這個 buffer 還需要設定很多其他的 meta data,這些 meta data 全部封裝在 log_t 結構體中。

問題6:redo log順序寫入磁碟?

redo log以順序的方式寫入檔案,當全部檔案寫滿的時候則回到第一個檔案相應的起始位置進行覆蓋寫,每次提交事務之後,都先將相關的操作紀錄檔寫入redo紀錄檔檔案中,並且都追加到檔案末尾,這是一個順序I/O

圖中展示了一組 4 個檔案的 redo log 紀錄檔,checkpoint 是當前要擦除的位置,擦除記錄前需要先把對應的資料落盤(更新記憶體頁,等待刷髒頁)。write pos 到 checkpoint 之間的部分可以用來記錄新的操作,如果 write pos 和 checkpoint 相遇,說明 redolog 已滿,這個時候資料庫停止進行資料庫更新語句的執行,轉而進行 redo log 紀錄檔同步到磁碟中。checkpoint 到 write pos 之間的部分等待落盤(先更新記憶體頁,然後等待刷髒頁)。

有了 redo log 紀錄檔,那麼在資料庫進行異常重新啟動的時候,可以根據 redo log 紀錄檔進行恢復,也就達到了 crash-safe。

redo log 用於保證 crash-safe 能力。innodb_flush_log_at_trx_commit 這個引數設定成 1 的時候,表示每次事務的 redo log 都直接持久化到磁碟。這個引數建議設定成 1,這樣可以保證 MySQL 異常重新啟動之後資料不丟失

2、bin log

MySQL 整體來看,其實就有兩塊:一塊是 Server 層,它主要做的是 MySQL 功能層面的事情;還有一塊是引擎層,負責儲存相關的具體事宜。上面我們聊到的 redo log 是 InnoDB 引擎特有的紀錄檔,而 Server 層也有自己的紀錄檔,稱為 binlog(歸檔紀錄檔)

為什麼會有兩份紀錄檔呢?

因為最開始 MySQL 裡並沒有 InnoDB 引擎。MySQL 自帶的引擎是 MyISAM,但是 MyISAM 沒有 crash-safe 的能力,binlog 紀錄檔只能用於歸檔。而 InnoDB 是另一個公司以外掛形式引入 MySQL 的,既然只依靠 binlog 是沒有 crash-safe 能力的,所以 InnoDB 使用另外一套紀錄檔系統——也就是 redo log 來實現 crash-safe 能力。

這兩種紀錄檔有以下三點不同。

① redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 層實現的,所有引擎都可以使用。

② redo log 是物理紀錄檔,記錄的是「在某個資料頁上做了什麼修改」;binlog 是邏輯紀錄檔,記錄的是這個語句的原始邏輯,比如「給 ID=2 這一行的 c 欄位加 1 」。

③ redo log 是迴圈寫的,空間固定會用完;binlog 是可以追加寫入的。「追加寫」是指 binlog 檔案寫到一定大小後會切換到下一個,並不會覆蓋以前的紀錄檔。

有了對這兩個紀錄檔的概念性理解後,再來看執行器和 InnoDB 引擎在執行這個 update 語句時的內部流程。

① 執行器先找引擎取 ID=2 這一行。ID 是主鍵,引擎直接用樹搜尋找到這一行。如果 ID=2 這一行所在的資料頁本來就在記憶體中,就直接返回給執行器;否則,需要先從磁碟讀入記憶體,然後再返回。

② 執行器拿到引擎給的行資料,把這個值加上 1,比如原來是 N,現在就是 N+1,得到新的一行資料,再呼叫引擎介面寫入這行新資料。

③ 引擎將這行新資料更新到記憶體(InnoDB Buffer Pool)中,同時將這個更新操作記錄到 redo log 裡面,此時 redo log 處於 prepare 狀態。然後告知執行器執行完成了,隨時可以提交事務。

④ 執行器生成這個操作的 binlog,並把 binlog 寫入磁碟。

⑤ 執行器呼叫引擎的提交事務介面,引擎把剛剛寫入的 redo log 改成提交(commit)狀態,更新完成

其中將 redo log 的寫入拆成了兩個步驟:prepare 和 commit,這就是兩階段提交(2PC)

問題1: 兩階段提交原理?

MySQL 使用兩階段提交主要解決 binlog 和 redo log 的資料一致性的問題。

兩階段提交原理描述:

① redo log 寫盤,InnoDB 事務進入 prepare 狀態。

② 如果前面 prepare 成功,binlog 寫盤,那麼再繼續將事務紀錄檔持久化到 binlog,如果持久化成功,那麼 InnoDB 事務則進入 commit 狀態 。

redo log 和 binlog 有一個共同的資料欄位,叫 XID。崩潰恢復的時候,會按順序掃描 redo log:

① 如果碰到既有 prepare、又有 commit 的 redo log,就直接提交;

② 如果碰到只有 parepare、而沒有 commit 的 redo log,就拿著 XID 去 binlog 找對應的事務。

binlog無記錄,回滾事務

binlog有記錄,提交事務

問題2:為什麼必須有「兩階段提交」呢?

如果不使用兩階段提交,假設當前 ID=2 的行,欄位 c 的值是 0,再假設執行 update 語句過程中在寫完第一個紀錄檔後,第二個紀錄檔還沒有寫完期間發生了 crash,會出現什麼情況呢?

**先寫 redo log 後寫 binlog。**假設在 redo log 寫完,binlog 還沒有寫完的時候,MySQL 程序異常重新啟動。由於我們前面說過的,redo log 寫完之後,系統即使崩潰,仍然能夠把資料恢復回來,所以恢復後這一行 c 的值是 1。

但是由於 binlog 沒寫完就 crash 了,這時候 binlog 裡面就沒有記錄這個語句。因此,之後備份紀錄檔的時候,存起來的 binlog 裡面就沒有這條語句。

然後你會發現,如果需要用這個 binlog 來恢復臨時庫的話,由於這個語句的 binlog 丟失,這個臨時庫就會少了這一次更新,恢復出來的這一行 c 的值就是 0,與原庫的值不同。

**先寫 binlog 後寫 redo log。**如果在 binlog 寫完之後 crash,由於 redo log 還沒寫,崩潰恢復以後這個事務無效,所以這一行 c 的值是 0。但是 binlog 裡面已經記錄了「把 c 從 0 改成 1」這個紀錄檔。所以,在之後用 binlog 來恢復的時候就多了一個事務出來,恢復出來的這一行 c 的值就是 1,與原庫的值不同。

可以看到,如果不使用「兩階段提交」,那麼資料庫的狀態就有可能和用它的紀錄檔恢復出來的庫的狀態不一致。

簡單說,redo log 和 binlog 都可以用於表示事務的提交狀態,而兩階段提交就是讓這兩個狀態保持邏輯上的一致。

3、undo log 實現原子性

undo log有兩個作用:提供回滾和多版本控制(MVCC)

在資料修改的時候,不僅記錄了redo,還記錄了相對應的undo,undo log主要記錄的是資料的邏輯變化,為了在發生錯誤時回滾之前的操作,需要將之前的操作都記錄下來,然後在發生錯誤時才可以回滾。

undo紀錄檔,只將資料庫邏輯地恢復到原來的樣子,在回滾的時候,它實際上是做的相反的工作,比如一條INSERT ,對應一條 DELETE,對於每個UPDATE,對應一條相反的 UPDATE,將修改前的行放回去。undo紀錄檔用於事務的回滾操作進而保障了事務的原子性。

實現原子性的關鍵,是當事務回滾時能夠復原所有已經成功執行的sql語句。 InnoDB 實現回滾,靠的是undo log :當事務對資料庫進行修改時,InnoDB 會生成對應的undo log 如果事務執行失敗或呼叫了rollback,導致事務需要回滾,便可以利用undo log中的資訊將資料回滾到修改之前的樣子。

在InnoDB儲存引擎中,undo log分為:

insert undo log

update undo log

insert undo log是指在insert 操作中產生的undo log,因為insert操作的記錄,只對事務本身可見,對其他事務不可見。故該undo log可以在事務提交後直接刪除,不需要進行purge操作。

而update undo log記錄的是對delete 和update操作產生的undo log,該undo log可能需要提供MVCC機制,因此不能再事務提交時就進行刪除。提交時放入undo log連結串列,等待purge執行緒進行最後的刪除。

補充:purge執行緒兩個主要作用是:清理undo頁和清除page裡面帶有Delete_Bit標識的資料行。在InnoDB中,事務中的Delete操作實際上並不是真正的刪除掉資料行,而是一種Delete Mark操作,在記錄上標識Delete_Bit,而不刪除記錄。是一種"假刪除",只是做了個標記,真正的刪除工作需要後臺purge執行緒去完成。

innodb中通過B+樹作為索引的資料結構,並且主鍵所在的索引為ClusterIndex(聚簇索引), ClusterIndex中的葉子節點中儲存了對應的資料內容。一個表只能有一個主鍵,所以只能有一個聚簇索引,如果表沒有定義主鍵,則選擇第一個非NULL唯一索引作為聚簇索引,如果還沒有則生成一個隱藏id列作為聚簇索引。

除了Cluster Index外的索引是Secondary Index(輔助索引)。輔助索引中的葉子節點儲存的是聚簇索引的葉子節點的值。

InnoDB行記錄中除了剛才提到的rowid外,還有trx_id和db_roll_ptr, trx_id表示最近修改的事務的id,db_roll_ptr指向undo segment中的undo log。

新增一個事務時事務id會增加,trx_id能夠表示事務開始的先後順序。

Undo log分為Insert和Update兩種,delete可以看做是一種特殊的update,即在記錄上修改刪除標記。

update undo log記錄了資料之前的資料資訊,通過這些資訊可以還原到之前版本的狀態。

當進行插入操作時,生成的Insert undo log在事務提交後即可刪除,因為其他事務不需要這個undo log。

進行刪除修改操作時,會生成對應的undo log,並將當前資料記錄中的db_roll_ptr指向新的undo log

4、MVCC實現隔離性

MVCC (MultiVersion Concurrency Control) 叫做多版本並行控制。

InnoDB的 MVCC ,是通過在每行記錄的後面儲存兩個隱藏的列來實現的。這兩個列, 一個儲存了行的建立時間,一個儲存了行的過期時間, 當然儲存的並不是實際的時間值,而是系統版本號。

主要實現思想是通過資料多版本來做到讀寫分離。從而實現不加鎖讀進而做到讀寫並行。

MVCC在mysql中的實現依賴的是undo log與read view

  • undo log :undo log 中記錄某行資料的多個版本的資料。
  • read view :用來判斷當前版本資料的可見性

InnoDB 在實現 MVCC 時用到的一致性讀檢視,即 consistent read view,用於支援 RC(Read Committed,讀提交)和 RR(Repeatable Read,可重複讀)隔離級別的實現。

在可重複讀隔離級別下,事務在啟動的時候就「拍了個快照」。

MySQL的MVCC快照並不是每一個事務進來就copy一份資料庫資訊,而是基於資料表每行資訊後面儲存的系統版本號去實現的。如下圖所示,一行資訊會有多個版本並存,每個事務可能讀取到的版本不一樣

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

而每行資料也都是有多個版本的。每次事務更新資料的時候,都會生成一個新的資料版本,並且把 transaction id 賦值給這個資料版本的row trx_id 。同時,舊的資料版本要保留,並且在新的資料版本中,能夠有資訊可以直接拿到它 。

資料表中的一行記錄,其實可能有多個版本 (row),每個版本有自己的 row trx_id。 就是一個記錄被多個事務連續更新後的狀態。

圖中虛線框裡是同一行資料的 4 個版本,當前最新版本是 V4,k 的值是 22,它是被 transaction id 為 25 的事務更新的,因此它的 row trx_id 也是 25。

語句更新會生成 undo log(回滾紀錄檔)嗎?那麼,undo log 在哪呢?

實際上,圖 2 中的三個虛線箭頭,就是 undo log;而 V1、V2、V3 並不是物理上真實存在的,而是每次需要的時候根據當前版本和 undo log 計算出來的。比如,需要 V2 的時候,就是通過 V4 依次執行 U3、U2 算出來。

按照可重複讀的定義,一個事務啟動的時候,能夠看到所有已經提交的事務結果。但是之後,這個事務執行期間,其他事務的更新對它不可見。因此,一個事務只需要在啟動的時候宣告說,「以我啟動的時刻為準,如果一個資料版本是在我啟動之前生成的,就認;如果是我啟動以後才生成的,我就不認,我必須要找到它的上一個版本」。當然,如果「上一個版本」也不可見,那就得繼續往前找。還有,如果是這個事務自己更新的資料,它自己還是要認的。

5、MySQL 鎖技術

當有多個請求來讀取表中的資料時可以不採取任何操作,但是多個請求裡有讀請求,又有修改請求時必須有一種措施來進行並行控制。不然很有可能會造成不一致。 讀寫鎖 解決上述問題很簡單,只需用兩種鎖的組合來對讀寫請求進行控制即可,

這兩種鎖被稱為:

共用鎖(shared lock),又叫做"讀鎖" 讀鎖是可以共用的,或者說多個讀請求可以共用一把鎖讀資料,不會造成阻塞。

排他鎖(exclusive lock),又叫做"寫鎖" 寫鎖會排斥其他所有獲取鎖的請求,一直阻塞,直到寫入完成釋放鎖。

總結: 通過讀寫鎖,可以做到讀讀可以並行,但是不能做到寫讀,寫寫並行 事務的隔離性就是根據讀寫鎖來實現的!!!

推薦學習:

以上就是一起來分析MySQL事務工作流程原理的詳細內容,更多請關注TW511.COM其它相關文章!