一條更新語句的執行流程
從一個表的一條更新語句說起,下面是這個表的建立語句,這個表有一個主鍵ID和一個整型字段c
mysql> create table T(ID int primary key, c int);
假如将ID=2這一行的值加1
mysql> update T set c=c+1 where ID=2;
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL90TQkZHeXpla1cVWzh2RhVDbzwEMW1mY1RzRapnTtxkb5ckYplTeMZTTINGMShUYfRHelRHLwEzX39GZhh2css2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xyayFWbyVGdhd3LcV2Zh1Wa9M3clN2byBXLzN3btg3Pn5GcuYjMyATO0QTM0ETNwkTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
執行語句要先通過連接配接器連接配接資料庫。在一個表上更新的時候,跟這個表有關的查詢緩存會失效。
接下來分析器會通過詞法和文法解析知道這是一條更新語句。優化器決定要使用ID這個索引。然後,執行器負責執行,找到這一行然後更新。
與查詢不同,更新和涉及到兩個重要的日志子產品:redo log(重做日志)和binlog(歸檔日志)。
redo log 重做日志
WAL技術(Write-Ahead Logging):先寫日志,再寫磁盤。因為如果每次都直接寫磁盤的話會效率很低很慢,是以等不忙的時候再寫入磁盤。
具體,就是當有一條記錄需要更新的時候,InnoDB引擎會先把記錄寫到redo log裡面,并更新記憶體。這個時候更新算完成。然後引擎會在适當的時候,再将這個操作記錄更新到磁盤裡,這時往往是系統比較空閑的時候。
但是redo log是固定的大小。有時候,假如記憶體太滿,就必須先将一部分記憶體更新到磁盤,再将這部分擦掉,然後在記憶體中寫入新的操作
假如說可以配置為一組4個檔案,每個大小1G,那麼總共可以記錄4GB的操作。write pos是目前記錄的位置,一邊寫一邊後移,寫到第3号檔案末尾就回到了0号檔案開頭。checkpoint是目前要擦除的位置,也是往後推移并循環。
write pos和checkpoint之間是還空着的部分,可以記錄新的操作。如果write pos追上checkpoint,這時代表redo log已滿,得先擦掉一些記錄,把checkpoint推進一下。
redo log可以使資料庫即使發生異常重新開機,之前送出的記錄也不會丢失,這稱為crash-safe。
binlog 歸檔日志
redo log是InnoDB引擎特有的日志,而Server層自己的日志稱為binlog,所有引擎都可使用。
redo log是實體日志,記錄“在某資料頁做了什麼修改”。binlog是邏輯日志,記錄這個語句的原始邏輯,“給ID=2這行的c字段加1”
redo log是循環寫,空間固定會用完。binlog是追加寫入,指檔案寫到一定大小會切換到下一個,并不會覆寫。
接下來是更新流程:
1.執行器先找到引擎取ID=2這一行。ID是主鍵,引擎通過樹搜尋找到這一行。如果這一行的資料頁本來就在記憶體,就直接傳回給執行器;否則需要先從磁盤讀入記憶體,再傳回。
2.執行器給這個值加1,再調用引擎接口寫入這行新資料。
3.引擎将這行資料更新到記憶體,同時将這個更新操作記錄到redo log。此時redo log會處于prepare狀态,告知執行器執行完成,随時可送出。
4.執行器生成這個操作的binlog,并把binlog寫入磁盤。
5.執行器調用引擎送出的事務接口,引擎把剛剛寫入的redo log改為commit狀态,更新完成。
(淺色為InnoDB内部執行,深色表示執行器中執行)
redo log的寫入在最後被binlog拆為兩個步驟:prepare和commit,這就是兩階段送出。
兩階段送出
怎麼讓資料庫恢複到半個月内任意一秒的狀态?
這意味着備份系統中儲存着最近半個月所有的binlog,同時系統會做定期的整庫備份。可以一天一備,也可一周一備。
一天一備對比一周一備的好處是“最長恢複時間”更短。最壞的情況是需要應用一天的binlog。比如每天0點做全量備份,而要恢複出一個到昨晚23點的備份。一周一備最壞的情況就是用一周的binlog。
當然更頻繁的全量備份需要消耗更多存儲空間。需要根據業務重要性來評估。
比如發現某天下午兩點發現中午十二點有一次誤删表,需要找回資料,那可以:
首先找到最近一次全庫備份,如果運氣好就是昨晚一個備份,從這個備份恢複到臨時庫。
然後從備份的時間點開始,将備份的binlog依次取出,重放到中午誤删那個時刻。
然後就可以将表資料從臨時庫取出來,按需要恢複到線上庫。
假如不是兩階段:
1.先寫redo log後寫binlog。假設在redo log寫完還沒寫binlog,也就是redo log寫完即使崩潰,仍然能夠把資料庫恢複回來,這一行的c就是加完後的1。但是binlog沒有寫完,存起來的binlog就沒有這條語句。如果後來用binlog恢複臨時庫,由于這句缺失,導緻臨時庫恢複的這行就是0,與原庫不同。
2.先寫binlog後寫redo log。假如binlog寫完後crash,redo log還沒寫,崩潰後恢複這行c還是0。但是binlog裡面已經記錄了把c從0改為1這個日志,是以之後用binlog來恢複就多出一個事務,恢複的c就是1,與原庫不同。
可以看到如果不是兩階段送出,那麼資料庫的狀态可能就和用它的日志恢複出來的不一緻。
兩階段送出就是讓這兩個狀态保持邏輯上的一緻。