本篇文章為大家帶來了關於mysql的相關知識,其中主要介紹了事務工作流程原理的相關問題,包括了事務的原子性是透過undo log來實現的、事務持久的是透過redo log來實現的等等內容,下面一起來看一下,希望對大家有幫助。
推薦學習:mysql影片教學
##問題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 異常重啟之後資料不遺失
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 都可以用來表示交易的提交狀態,而兩階段提交就是讓這兩個狀態保持邏輯上的一致。
#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
MVCC (MultiVersion Concurrency Control) 叫做多版本並發控制。
InnoDB的 MVCC ,是透過在每行記錄的後面保存兩個隱藏的列來實現的。這兩個列, 一個保存了行的創建時間,一個保存了行的過期時間, 當然存儲的並不是實際的時間值,而是系統版本號。
主要實現想法是透過資料多版本來做到讀寫分離。從而實現不加鎖讀進而做到讀寫並行。
MVCC在mysql中的實作依賴的是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影片教學
以上是一起來分析MySQL事務工作流程原理的詳細內容。更多資訊請關注PHP中文網其他相關文章!