1 事前
- 建立一個示例表
-
阿裡三面當場懵逼:MySQL執行更新語句時做了什麼?1 事前2 SQL語句基本執行流程3 redo log4 binlog日志對比兩階段送出 - 插倆條資料
-
阿裡三面當場懵逼:MySQL執行更新語句時做了什麼?1 事前2 SQL語句基本執行流程3 redo log4 binlog日志對比兩階段送出 - 更新一條資料
-
阿裡三面當場懵逼:MySQL執行更新語句時做了什麼?1 事前2 SQL語句基本執行流程3 redo log4 binlog日志對比兩階段送出
2 SQL語句基本執行流程
同樣适用于更新語句。
- 執行語句前,先通過連接配接器連接配接DB
- 表上有更新時,此表有關查詢緩存就會失效,是以該語句就會把表中的所有緩存置空
- 分析器通過詞法、文法解析,哦原來這是一條更新語句
- 優化器決定使用id索引
- 執行器負責具體執行,找到這一行,更新之
與查詢流程不同的是更新過程涉及兩個日志子產品:
- redo log(重做日志)
- binlog(歸檔日志)
3 redo log
賒賬或還賬時,一般有兩種做法:
- 直接拿出小本本,把這次賒的賬加上或扣掉
- 先在借條記下這次的賬,等晚上下班了,再把小本本翻出來核算
在日常 996 強度下,我們肯定選後者,因為前者太麻煩:
首先,得從一堆記錄找到這個人的賒賬記錄
找到了,再拿出電腦計算
最後,再将結果寫回小本本
是以還是先在借條記一下友善。若我沒有借條,每次記賬都拿出小本本,效率豈不是低死啦?
MySQL也面對這種問題:若每次更新操作都直接寫進磁盤,然後磁盤也要找到對應記錄,然後再更新,整個過程I/O成本、搜尋成本都很高。
何解?就采用我的借條思路。
借條和小本本的協作過程,就是MySQL的WAL(Write-Ahead Logging)。關鍵就在于:
先寫日志(先寫借條)
再寫磁盤(不忙時,寫小本本)
當一條記錄需要更新,InnoDB先把記錄寫到redo log(借條),并更新記憶體,更新就算完成了。
InnoDB在适當時,将操作記錄更新到磁盤,而這個更新往往是在系統比較空閑的時候做,這就像打烊以後掌櫃做的事。
若今天欠錢的不多,我可以忙完了再整理。若某天欠錢的多了,借條寫滿了,怎麼辦?
放下手中活兒,把借條一部分欠款記錄更新到賬本,然後把這些記錄從借條擦掉,為記新賬騰出空間。
類似的InnoDB的redo log固定大小,比如可配置為一組4個檔案,每個檔案的大小是1GB,那麼這塊“借條”總共就可以記錄4GB的操作。
從頭開始寫,寫到末尾就又回到開頭循環寫
write pos是目前記錄位置,一邊寫一邊後移,寫到第3号檔案末尾後回到0号檔案開頭
checkpoint是目前要擦除位置,往後推移并且循環,擦除記錄前要把記錄更新到資料檔案
write pos和checkpoint之間是“借條”上還空着的部分,用來記錄新操作。若write pos追上checkpoint,“借條”滿,不能再執行新的更新,得停下來先擦掉一些記錄,checkpoint推進。
redo log可以保證即使資料庫發生異常重新開機,不會丢失之前送出的記錄,這叫crash-safe。
隻要欠款記錄記在借條或寫在小本本上,之後即使我忘記了,比如突然停業,恢複生意後依然可以通過賬本和粉闆上的資料明确賒賬賬目。
4 binlog
- MySQL Server層,負責MySQL功能層面
- 引擎層,負責存儲相關
- 借條redo log是InnoDB引擎特有的日志,而Server層也有自己的日志,稱為binlog(歸檔日志)。
為什麼需要兩份日志?
最開始MySQL并無InnoDB,自帶的是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是主鍵,引擎直接用b+樹搜尋。這一行所在資料頁本就在記憶體,則直接傳回給執行器;否則先從磁盤讀入記憶體,再傳回
執行器拿到引擎給的行資料,把這個值加1,得到新的一行資料,再調用引擎接口寫入這行新資料
引擎将這行新資料更新到記憶體,同時将更新操作記錄到redo log,此時redo log處prepare态。然後告知執行器執行完成,随時可以送出事務
執行器生成這個操作的binlog,并把binlog寫入磁盤
執行器調用引擎的送出事務接口,引擎把剛剛寫入的redo log改成送出(commit)狀态,更新完成
-
淺色框表示是在InnoDB内部執行
深色框表示是在執行器執行
最後三步看上去有點“繞”,将redo log的寫入拆成了兩個步驟:prepare和commit,這就是"兩階段送出"。
兩階段送出
為什麼必須有“兩階段送出”?
為了讓兩份日志之間的邏輯一緻。
思考怎樣讓資料庫恢複到半個月内任意一秒的狀态?
binlog會記錄所有的邏輯操作,并且采用“追加寫”。如果DBA承諾說半個月内可以恢複,那麼備份系統中一定會儲存最近半個月的所有binlog,同時系統會定期做整庫備份。
“定期”取決于系統的重要性,可以是一天一備,也可以是一周一備。
資料恢複過程
當需要恢複到指定秒時,比如某天下午兩點發現中午十二點有次誤删表,需要找回資料:
- 找到最近的一次全量備份。若運氣好,可能就是昨天晚上備份的,從這個備份恢複到臨時庫
- 從備份時間點開始,将備份的binlog依次取出來,重放到中午誤删表之前的那個時刻
這樣你的臨時庫就跟誤删之前的線上庫一樣了,然後你可以把表資料從臨時庫取出來,按需要恢複到線上庫去。
為什麼需要“兩階段送出”
由于redo log和binlog是兩個獨立的邏輯,如果不用兩階段送出,要麼就是先寫完redo log再寫binlog,或者反序。
假設目前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,與原庫的值不同。
是以不使用“兩階段送出”,則DB狀态就有可能和用它的日志恢複出來的庫的狀态不一緻。
機率是不是很低,平時也沒有什麼動不動就需要恢複臨時庫的場景呀?不是的,不隻是誤操作後需要用這個過程來恢複資料。需要擴容時,需要再多搭建一些備庫來增加系統的讀能力的時候,現在常見的做法也是用全量備份加上應用binlog來實作的,這個“不一緻”就會導緻你的線上出現主從資料庫不一緻的情況。
redo log和binlog都可用于表示事務的送出狀态,而兩階段送出就是讓這兩個狀态保持邏輯上的一緻。
innodb_flush_log_at_trx_commit設成1,表示每次事務的redo log都直接持久化到磁盤。建議設成1,保證MySQL異常重新開機之後資料不丢失
sync_binlog這個參數設定成1的時候,表示每次事務的binlog都持久化到磁盤。建議設成1,保證MySQL異常重新開機之後binlog不丢失
參考
https://segmentfault.com/a/1190000023827696