久久精品人人爽,华人av在线,亚洲性视频网站,欧美专区一二三

MySQL持久化和回滾該怎么理解

175次閱讀
沒有評論

共計 7810 個字符,預計需要花費 20 分鐘才能閱讀完成。

這篇文章跟大家分析一下“MySQL 持久化和回滾該怎么理解”。內容詳細易懂,對“MySQL 持久化和回滾該怎么理解”感興趣的朋友可以跟著丸趣 TV 小編的思路慢慢深入來閱讀一下,希望閱讀后能夠對大家有所幫助。下面跟著丸趣 TV 小編一起深入學習“MySQL 持久化和回滾該怎么理解”的知識吧。

redo log

事務的支持是數據庫區分文件系統的重要特征之一,事務的四大特性:

原子性:所有的操作要么都做,要么都不做,不可分割。

一致性:數據庫從一種狀態變成另一種狀態的的結果最終是一致的,比如 A 給 B 轉賬 500,A 最終少了 500,B 最終多了 500,但是 A + B 的值始終沒變。

隔離性:事務和事務之前相互隔離,互不干擾。

持久性:事務一旦提交,它對數據的變更是永久性的。

本篇文章主要說說持久性相關的知識。

當我們在事務中更新一條記錄的時候,比如:

update user set age=11 where user_id=1;

它的流程大概是這樣的:

先判斷 user_id 這條數據所在的頁是否在內存里,如果不在的話,先從數據庫讀取到,然后加載到內存中

修改內存中的 age 為 11

寫入 redo log,并且 redo log 處于 prepare 狀態

寫入 binlog

提交事務,redo log 變成 commit 狀態

這里面有幾個關鍵的點:redo log 是什么?為什么需要 redo log?prepare 狀態的 redo log 是什么?redo log 和 binlog 是否可以只選其一 …? 帶著這一系列的問題,我們來揭開 redo log 的面紗。

為什么要先更新內存數據,不直接更新磁盤數據?

我們為什么不每次更新數據的時候,直接更新對應的磁盤數據?首先我們知道磁盤 IO 是緩慢的,內存是快速的,兩者的速度不是一個量級的,那么針對緩慢的磁盤 IO,出現了索引,通過索引哪怕數據成百上千萬我們依然可以在磁盤上很快速的找我們的數據,這就是索引的作用。但是索引也需要維護,并不是一成不變的,當我們插入一條新數據 A 的時候,由于這條數據要插入在已存在的數據 B 之后,那么就要移動 B 數據,讓出一個位置給 A,這個有一定的開銷。

更糟糕的是,本來要插入的頁已經滿了,那么就要申請一個新的頁,然后挪一部分數據過去,這叫做頁的分裂,這個開銷更大。如果我們的 sql 變更是直接修改磁盤的數據,恰巧正好出現上面的問題,那么此時的效率就會很低,嚴重的話會造成超時,這也是上面更新的過程為什么先要加載對應的數據頁到內存中,然后先更新內存中的數據的原因。對于 mysql 來說,所有的變更都必須先更新緩沖池中的數據,然后緩沖池中的臟頁會以一定的頻率被刷入磁盤(checkPoint 機制),通過緩沖池來優化 CPU 和磁盤之間的鴻溝,這樣就可以保證整體的性能不會下降太快。

為什么需要 redo log?

緩沖池可以幫助我們消除 CPU 和磁盤之間的鴻溝,checkpoint 機制可以保證數據的最終落盤,然而由于 checkpoint 并不是每次變更的時候就觸發的,而是 master 線程隔一段時間去處理的。所以最壞的情況就是剛寫完緩沖池,數據庫宕機了,那么這段數據就是丟失的,無法恢復。這樣的話就不滿足 ACID 中的 D,為了解決這種情況下的持久化問題,InnoDB 引擎的事務采用了 WAL 技術(Write-Ahead Logging),這種技術的思想就是先寫日志,再寫磁盤,只有日志寫入成功,才算事務提交成功,這里的日志就是 redo log。當發生宕機且數據未刷到磁盤的時候,可以通過 redo log 來恢復,保證 ACID 中的 D,這就是 redo log 的作用。

redo log 是如何實現的?

redo log 的寫入并不是直接寫入磁盤的,redo log 也有緩沖區的,叫做 redo log buffer(重做日志緩沖),InnoDB 引擎會在寫 redo log 的時候先寫 redo log buffer,然后也是以一定的頻率刷入到真正的 redo log 中,redo log buffer 一般不需要特別大,它只是一個臨時的容器,master 線程會每秒將 redo log buffer 刷到 redo log 文件中,因此我們只要保證 redo log buffer 能夠存下 1s 內的事務變更的數據量即可,以 mysql5.7.23 為例,這個默認是 16M。

mysql  show variables like  %innodb_log_buffer_size% 
+------------------------+----------+
| Variable_name | Value |
+------------------------+----------+
| innodb_log_buffer_size | 16777216 |
+------------------------+----------+

16M 的 buffer 足夠應對大部分應用了,buffer 同步到 redo log 的策略主要有如下幾個:

master 線程每秒將 buffer 刷到到 redo log 中

每個事務提交的時候會將 buffer 刷到 redo log 中

當 buffer 剩余空間小于 1 / 2 時,會被刷到 redo log 中

需要注意的是 redo log buffer 刷到 redo log 的過程并不是真正的刷到磁盤中去了,只是刷入到 os cache 中去,這是現代操作系統為了提高文件寫入的效率做的一個優化,真正的寫入會交給系統自己來決定(比如 os cache 足夠大了)。那么對于 InnoDB 來說就存在一個問題,如果交給系統來 fsync,同樣如果系統宕機,那么數據也丟失了(雖然整個系統宕機的概率還是比較小的)。針對這種情況,InnoDB 給出 innodb_flush_log_at_trx_commit 策略,讓用戶自己決定使用哪個。

mysql  show variables like  innodb_flush_log_at_trx_commit 
+--------------------------------+-------+
| Variable_name | Value |
+--------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1 |
+--------------------------------+-------+

0:表示事務提交后,不進行 fsync,而是由 master 每隔 1s 進行一次重做日志的 fysnc

1:默認值,每次事務提交的時候同步進行 fsync

2:寫入 os cache 后,交給操作系統自己決定什么時候 fsync

從 3 種刷入策略來說:

2 肯定是效率最高的,但是只要操作系統發生宕機,那么就會丟失 os cache 中的數據,這種情況下無法滿足 ACID 中的 D

0 的話,是一種折中的做法,它的 IO 效率理論是高于 1 的,低于 2 的,它的數據安全性理論是要低于 1 的,高于 2 的,這種策略也有丟失數據的風險,也無法保證 D。

1 是默認值,可以保證 D,數據絕對不會丟失,但是效率最差的。個人建議使用默認值,雖然操作系統宕機的概率理論小于數據庫宕機的概率,但是一般既然使用了事務,那么數據的安全應該是相對來說更重要些。

redo log 是對頁的物理修改,第 x 頁的第 x 位置修改成 xx,比如:

page(2,4),offset 64,value 2

在 InnoDB 引擎中,redo log 都是以 512 字節為單位進行存儲的,每個存儲的單位我們稱之為 redo log block(重做日志塊),若一個頁中存儲的日志量大于 512 字節,那么就需要邏輯上切割成多個 block 進行存儲。

一個 redo log block 是由日志頭、日志體、日志尾組成。日志頭占用 12 字節,日志尾占用 8 字節,所以一個 block 真正能存儲的數據就是 512-12-8=492 字節。

多個 redo log block 組成了我們的 redo log。

每個 redo log 默認大小為 48M:

mysql  show variables like  innodb_log_file_size 
+----------------------+----------+
| Variable_name | Value |
+----------------------+----------+
| innodb_log_file_size | 50331648 |
+----------------------+----------+

InnoDB 默認 2 個 redo log 組成一個 log 組,真正工作的就是這個 log 組。

mysql  show variables like  innodb_log_files_in_group 
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| innodb_log_files_in_group | 2 |
+---------------------------+-------+
#ib_logfile0
#ib_logfile1

當 ib_logfile0 寫完之后,會寫 ib_logfile1,當 ib_logfile1 寫完之后,會重新寫 ib_logfile0…,就這樣一直不停的循環寫。

為什么一個 block 設計成 512 字節?

這個和磁盤的扇區有關,機械磁盤默認的扇區就是 512 字節,如果你要寫入的數據大于 512 字節,那么要寫入的扇區肯定不止一個,這時就要涉及到盤片的轉動,找到下一個扇區,假設現在需要寫入兩個扇區 A 和 B,如果扇區 A 寫入成功,而扇區 B 寫入失敗,那么就會出現非原子性的寫入,而如果每次只寫入和扇區的大小一樣的 512 字節,那么每次的寫入都是原子性的。

為什么要兩段式提交?

從上文我們知道,事務的提交要先寫 redo log(prepare),再寫 binlog,最后再提交 (commit)。這里為什么要有個 prepare 的動作?redo log 直接 commit 狀態不行嗎?假設 redo log 直接提交,在寫 binlog 的時候,發生了 crash,這時 binlog 就沒有對應的數據,那么所有依靠 binlog 來恢復數據的 slave,就沒有對應的數據,導致主從不一致。

所以需要通過兩段式(2pc)提交來保證 redo log 和 binlog 的一致性是非常有必要的。具體的步驟是:處于 prepare 狀態的 redo log,會記錄 2PC 的 XID,binlog 寫入后也會記錄 2PC 的 XID,同時會在 redo log 上打上 commit 標識。

redo log 和 bin log 是否可以只需要其中一個?

不可以。redo log 本身大小是固定的,在寫滿之后,會重頭開始寫,會覆蓋老數據,因為 redo log 無法保存所有數據,所以在主從模式下,想要通過 redo log 來同步數據給從庫是行不通的。那么 binlog 是一定需要的,binlog 是 mysql 的 server 層產生的,和存儲引擎無關,binglog 又叫歸檔日志,當一個 binlog file 寫滿之后,會寫入到一個新的 binlog file 中。

所以我們是不是只需要 binlog 就行了?redo log 可以不需要?當然也不行,redo log 的作用是提供 crash-safe 的能力,首先對于一個數據的修改,是先修改緩沖池中的數據頁的,這時修改的數據并沒有真正的落盤,這主要是因為磁盤的離散讀寫能力效率低,真正落盤的工作交給 master 線程定期來處理,好處就是 master 可以一次性把多個修改一起寫入磁盤。

那么此時就有一個問題,當事務 commit 之后,數據在緩沖區的臟頁中,還沒來的及刷入磁盤,此時數據庫發生了崩潰,那么這條 commit 的數據即使在數據庫恢復后,也無法還原,并不能滿足 ACID 中的 D,然后就有了 redo log,從流程來看,一個事務的提交必須保證 redo log 的寫入成功,只有 redo log 寫入成功才算事務提交成功,redo log 大部分情況是順序寫的磁盤,所以它的效率要高很多。當 commit 后發生 crash 的情況下,我們可以通過 redo log 來恢復數據,這也是為什么需要 redo log 的原因。

但是事務的提交也需要 binlog 的寫入成功,那為什么不可以通過 binlog 來恢復未落盤的數據?這是因為 binlog 不知道哪些數據落盤了,所以不知道哪些數據需要恢復。對于 redo log 而言,在數據落盤后對應的 redo log 中的數據會被刪除,那么在數據庫重啟后,只要把 redo log 中剩下的數據都恢復就行了。

crash 后是如何恢復的?

通過兩段式提交我們知道 redo log 和 binlog 在各個階段會被打上 prepare 或者 commit 的標識,同時還會記錄事務的 XID,有了這些數據,在數據庫重啟的時候,會先去 redo log 里檢查所有的事務,如果 redo log 的事務處于 commit 狀態,那么說明在 commit 后發生了 crash,此時直接把 redo log 的數據恢復就行了,如果 redo log 是 prepare 狀態,那么說明 commit 之前發生了 crash,此時 binlog 的狀態決定了當前事務的狀態,如果 binlog 中有對應的 XID,說明 binlog 已經寫入成功,只是沒來的及提交,此時再次執行 commit 就行了,如果 binlog 中找不到對應的 XID,說明 binlog 沒寫入成功就 crash 了,那么此時應該執行回滾。

undo log

redo log 是事務持久性的保證,undo log 是事務原子性的保證。在事務中更新數據的前置操作其實是要先寫入一個 undo log 中的,所以它的流程大致如下:

什么情況下會生成 undo log?

undo log 的作用就是 mvcc(多版本控制)和回滾,我們這里主要說回滾,當我們在事務里 insert、update、delete 某些數據的時候,就會產生對應的 undo log,當我們執行回滾時,通過 undo log 就可以回到事務開始的樣子。需要注意的是回滾并不是修改的物理頁,而是邏輯的恢復到最初的樣子,比如一個數據 A,在事務里被你修改成 B,但是此時有另一個事務已經把它修改成了 C,如果回滾直接修改數據頁把數據改成 A,那么 C 就被覆蓋了。

對于 InnoDB 引擎來說,每個行記錄除了記錄本身的數據之外,還有幾個隱藏的列:

DB_ROW_ID:如果沒有為表顯式的定義主鍵,并且表中也沒有定義唯一索引,那么 InnoDB 會自動為表添加一個 row_id 的隱藏列作為主鍵。

DB_TRX_ID:每個事務都會分配一個事務 ID,當對某條記錄發生變更時,就會將這個事務的事務 ID 寫入 trx_id 中。

DB_ROLL_PTR:回滾指針,本質上就是指向 undo log 的指針。

當我們執行 INSERT 時:

begin;
INSERT INTO user (name) VALUES (tom)

插入的數據都會生一條 insert undo log,并且數據的回滾指針會指向它。undo log 會記錄 undo log 的序號、插入主鍵的列和值 …,那么在進行 rollback 的時候,通過主鍵直接把對應的數據刪除即可。

對于更新的操作會產生 update undo log,并且會分更新主鍵的和不更新的主鍵的,假設現在執行:

UPDATE user SET name= Sun  WHERE id=1;

這時會把老的記錄寫入新的 undo log,讓回滾指針指向新的 undo log,它的 undo no 是 1,并且新的 undo log 會指向老的 undo log(undo no=0)。

假設現在執行:

UPDATE user SET id=2 WHERE id=1;

對于更新主鍵的操作,會先把原來的數據 deletemark 標識打開,這時并沒有真正的刪除數據,真正的刪除會交給清理線程去判斷,然后在后面插入一條新的數據,新的數據也會產生 undo log,并且 undo log 的序號會遞增。

可以發現每次對數據的變更都會產生一個 undo log,當一條記錄被變更多次時,那么就會產生多條 undo log,undo log 記錄的是變更前的日志,并且每個 undo log 的序號是遞增的,那么當要回滾的時候,按照序號依次向前推,就可以找到我們的原始數據了。

undo log 是如何回滾的?

以上面的例子來說,假設執行 rollback,那么對應的流程應該是這樣:

通過 undo no= 3 的日志把 id= 2 的數據刪除

通過 undo no= 2 的日志把 id= 1 的數據的 deletemark 還原成 0

通過 undo no= 1 的日志把 id= 1 的數據的 name 還原成 Tom

通過 undo no= 0 的日志把 id= 1 的數據刪除

undo log 存在什么地方?

InnoDB 對 undo log 的管理采用段的方式,也就是回滾段,每個回滾段記錄了 1024 個 undo log segment,InnoDB 引擎默認支持 128 個回滾段

mysql  show variables like  innodb_undo_logs 
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| innodb_undo_logs | 128 |
+------------------+-------+

那么能支持的最大并發事務就是 128*1024。每個 undo log segment 就像維護一個有 1024 個元素的數組。

當我們開啟個事務需要寫 undo log 的時候,就得先去 undo log segment 中去找到一個空閑的位置,當有空位的時候,就會去申請 undo 頁,最后會在這個申請到的 undo 頁中進行 undo log 的寫入。我們知道 mysql 默認一頁的大小是 16k。

mysql  show variables like  %innodb_page_size% 
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+

那么為一個事務就分配一個頁,其實是非常浪費的(除非你的事物非常長),假設你的應用的 TPS 為 1000,那么 1s 就需要 1000 個頁,大概需要 16M 的存儲,1 分鐘大概需要 1G 的存儲 …,如果照這樣下去除非 mysql 清理的非常勤快,否則隨著時間的推移,磁盤空間會增長的非常快,而且很多空間都是浪費的。

于是 undo 頁就被設計的可以重用了,當事務提交時,并不會立刻刪除 undo 頁,因為重用,這個 undo 頁它可能不干凈了,所以這個 undo 頁可能混雜著其他事務的 undo log。undo log 在 commit 后,會被放到一個鏈表中,然后判斷 undo 頁的使用空間是否小于 3 /4,如果小于 3 / 4 的話,則表示當前的 undo 頁可以被重用,那么它就不會被回收,其他事務的 undo log 可以記錄在當前 undo 頁的后面。由于 undo log 是離散的,所以清理對應的磁盤空間時,效率不是那么高。

關于 MySQL 持久化和回滾該怎么理解就分享到這里啦,希望上述內容能夠讓大家有所提升。如果想要學習更多知識,請大家多多留意丸趣 TV 小編的更新。謝謝大家關注一下丸趣 TV 網站!

正文完
 
丸趣
版權聲明:本站原創文章,由 丸趣 2023-07-15發表,共計7810字。
轉載說明:除特殊說明外本站除技術相關以外文章皆由網絡搜集發布,轉載請注明出處。
評論(沒有評論)
主站蜘蛛池模板: 津市市| 哈尔滨市| 临沭县| 彭阳县| 宿松县| 永城市| 长乐市| 乌兰县| 揭西县| 兴安县| 玛多县| 永靖县| 丰县| 大埔县| 沧州市| 临洮县| 全南县| 阳春市| 黄骅市| 化德县| 保山市| 莱西市| 玛多县| 陈巴尔虎旗| 肥乡县| 铁岭县| 神农架林区| 襄垣县| 绥宁县| 长岛县| 福鼎市| 嘉荫县| 吴江市| 普洱| 阿勒泰市| 盐边县| 三门峡市| 雅安市| 新沂市| 南投市| 咸丰县|