本文準備通俗的講解MySQL的InnoDB存儲引擎事務的實現(xiàn)原理。
首先,我們知道事務具有ACID四個特性盗蟆。也即:原子性,一致性鉴未,隔離性,持久性。
這四個性質(zhì)我們不用干癟的文字去闡述,我們只需要知道事務保證了一系列的操作要么全部執(zhí)行殃姓,要么一個也不執(zhí)行,同時一旦事務提交瓦阐,則其所做的修改會永久保存到數(shù)據(jù)庫即可。
接下來我們一起看看InnoDB怎么實現(xiàn)的事務篷牌。
ACD三個特性是通過Redo log(重做日志)和Undo log 實現(xiàn)的睡蟋。 而隔離性是通過鎖來實現(xiàn)的。由于隔離性和鎖在之前的文章講過了枷颊。所以本文重點關注Redo log 和Undo log戳杀。
一、Redo log
重做日志用來實現(xiàn)事務的持久性夭苗,即D特性信卡。它由兩部分組成:
①內(nèi)存中的重做日志緩沖
②重做日志文件
一看有內(nèi)存和磁盤上的兩個對應實體,我們就知道這樣做一定是為了效率考慮题造,因為內(nèi)存的讀寫效率要比磁盤讀寫效率高太多傍菇。
Innodb是支持事務的存儲引擎,在事務提交時界赔,必須先將該事務的所有日志寫入到redo日志文件中丢习,待事務的commit操作完成才算整個事務操作完成。在每次將redo log buffer寫入redo log file后淮悼,都需要調(diào)用一次fsync操作咐低,因為重做日志緩沖只是把內(nèi)容先寫入操作系統(tǒng)的緩沖系統(tǒng)中,并沒有確保直接寫入到磁盤上袜腥,所以必須進行一次fsync操作见擦。因此,磁盤的性能在一定程度上也決定了事務提交的性能。
關于fsync這個操作用戶是可以干預的鲤屡,因為每次提交事務都執(zhí)行一次fsync儡湾,確實影響數(shù)據(jù)庫性能。通過innodb_flush_log_at_trx_commit來控制redo log刷新到磁盤的策略执俩。該參數(shù)的默認值為1徐钠,表示每次提交事務時都執(zhí)行一次fsync操作。0則表示事務提交時不進行寫入重做日志文件役首,這個寫入操作由master thread進程來完成尝丐,master thread每一秒會進行一次重做日志文件的fsync操作。2則表示事務提交時將重做日志寫入重做日志文件衡奥,但僅寫入文件系統(tǒng)的緩存中爹袁,并不進行fsync操作。用戶可以通過設置0或者2啦提高事務提交的性能矮固,也可以設置1來要求確保redo log是寫入文件中的失息,總之三種方法各有利弊。
還有需要了解的是:
redo log buffer將內(nèi)存中的log block刷新到磁盤是有一定的規(guī)則的:事務提交時(前面已經(jīng)提到)档址、當log buffer中有一半的內(nèi)存空間被使用時盹兢、log checkpoint時。
那接下來我們就需要看看redo log file存儲的內(nèi)容到底是什么了守伸。
為了避免大家懵圈绎秒,不打算把存儲格式一個一個細鉆(我也沒那實力,哈哈)尼摹。我們只需要知道他大致是怎么設計的就行了见芹。這樣,我們以后如果自己設計一個類似場景的產(chǎn)品蠢涝,就完全可以借鑒它的設計思想啦玄呛。
好,開始:
在InnoDB存儲引擎中和二,重做日志都是以512字節(jié)為單位進行存儲的徘铝,這意味著重做日志緩存、重做日志文件塊都是以塊(block)的方式進行保存的儿咱,稱為重做日志塊(redo log block)庭砍。每塊的大小512字節(jié)。由于重做日志塊的大小和磁盤扇區(qū)大小一樣混埠,都是512字節(jié)怠缸,因此重做日志的寫入可以保證原子性,不需要double write技術钳宪。
每個重做日志塊的內(nèi)容快除了日志記錄本身之外揭北,還由日志塊頭(log block header)及日志塊尾(log block tailer)兩部分組成扳炬。重做日志頭一共占用12字節(jié),重做日志尾占用8字節(jié)搔体。這兩部分是固定的恨樟。故每個重做日志塊實際可以存儲的大小為492字節(jié)(512-12-8),如下圖顯示重做日志塊緩存的結構:
在圖中標注出來不用太過關注這幾個字段的含義疚俱,因為他們對理解Redo log實現(xiàn)事務的機制沒有太大影響劝术,反而如果關注這些,容易讓人看到這些大寫字母的變量感到頭暈呆奕。
ps:這些變量是維護log block狀態(tài)的一些變量养晋。比如表示log block當前使用量,當前redo block的第一個redo log開始位置等等梁钾。舉個例子吧:
事務T1的重做日志1占用762字節(jié)绳泉,事務T2的重做日志占用100字節(jié),姆泻。由于每個log block實際只能保存492字節(jié)零酪,因此其在log buffer的情況應該如下圖所示:
實現(xiàn)這個功能就是靠log block的頭部的字段來實現(xiàn)的。好了拇勃,這不是我們關注的問題四苇,講這個只是為了滿足大家的好奇心以及對這些變量的初步認識。
重做日志塊中出去header和tailer的內(nèi)容就是具體的redo log了潜秋。不同的數(shù)據(jù)庫操作會有對應的重做日志格式蛔琅。此外,由于InnoDB存儲引擎的存儲管理是基于頁的峻呛,故其重做日志格式也是基于頁的。雖然有著不同的重做日志格式辜窑,但他們有著通用的頭部格式钩述,如圖:
通用的頭部格式由一下3部分組成
redo_log_type: 重做日志類型
space:表空間ID
page_no 頁的偏移量即頁的位置
之后是redo log body ,根據(jù)重做日志類型的不同穆碎,會有不同的存儲內(nèi)容牙勘,例如,對于頁上記錄的插入和刪除操作所禀,分別對應的如圖的格式(同樣方面,不要細扣每一個字段的含義,這不是我們要抓的重點):
大體上的redo log結構介紹完了色徘。在說從redo log file恢復之前恭金,還要說一個LSN的概念,LSN是Log Sequence Number的縮寫褂策,其代表的是日志序列號横腿,在InnoDB存儲引擎中颓屑,LSN占用8個字節(jié),并且單調(diào)遞增耿焊。
LSN表示事務寫入重做日志字節(jié)的總量揪惦。例如當前重做日志的LSN為1000,有一個事務T1寫入了100字節(jié)的重做日志罗侯,那么LSN就變成1100器腋,若又有事務T2寫入200字節(jié)的重做日志,那么LSN就變?yōu)?300钩杰。
LSN不僅記錄在重做日志中纫塌,還存在每個頁中,在每個頁的頭部榜苫,有一個值FIL_PAGE_LSN护戳,記錄了該頁的LSN,在頁中垂睬,LSN表示該頁最后刷新時LSN的大小媳荒。因為重做日志記錄的是每個頁的日志,因此頁中的LSN可以判斷頁是否需要進行恢復操作驹饺。例如钳枕,頁P1的LSN為10000,而數(shù)據(jù)庫啟動時赏壹,InnoDB檢測到寫入重做日志中的LSN為13000鱼炒,并且事務已經(jīng)提交,那么數(shù)據(jù)庫需要進行恢復操作蝌借。將重做日志應用到P1頁中昔瞧,同樣的,對于重做日志中LSN小于P1頁的LSN菩佑,不需要進行重做自晰,因為P1頁中的LSN表示已經(jīng)被刷新到該位置,在此位置之前的內(nèi)容已經(jīng)被成功的處理了稍坯。
接下來就是恢復操作了:
InnoDB存儲引擎在啟動時不管上次數(shù)據(jù)運行是否正常關閉酬荞,都會嘗試進行恢復操作,因為重做日志記錄的是物理日志(不要糾結這個)瞧哟,因此恢復的速度比邏輯日志混巧,如二進制日志要快的多,于此同時勤揩,InnoDB存儲引擎自身也對恢復進行了一定程度的優(yōu)化咧党,如順序讀取及并行應用重做日志,這樣可以進一步提高數(shù)據(jù)庫恢復的速度
由于checkpoint表示已經(jīng)刷新到磁盤頁上的LSN雄可,因此在恢復過程中僅需恢復checkpoint開始的日志部分凿傅。對于圖中的例子缠犀,當數(shù)據(jù)庫在checkpoint的LSN為10 000時發(fā)生宕機,恢復操作僅恢復LSN 10000~13000范圍內(nèi)的日志聪舒。
物理日志
舉個例子辨液,對于Insert操作,物理日志記錄的是每個頁的變化:
若執(zhí)行SQL語句:
INSERT INTO t SELECT 1,2;
其記錄的重做日志大致類似這個樣子:
page(2,3),offset 32,value 1,2
二箱残、Undo log
第二部分是Undo log滔迈,它可以實現(xiàn)如下兩個功能:
1.實現(xiàn)事務回滾
2.實現(xiàn)MVCC
undo log和redo log記錄物理日志不一樣,它是邏輯日志被辑×呛罚可以認為當delete一條記錄時,undo log中會記錄一條對應的insert記錄盼理,反之亦然谈山,當update一條記錄時,它記錄一條對應相反的update記錄宏怔。
當執(zhí)行回滾時奏路,就可以從undo log中的邏輯記錄讀取到相應的內(nèi)容并進行回滾。有時候應用到行版本控制的時候臊诊,也是通過undo log來實現(xiàn)的:當讀取的某一行被其他事務鎖定時鸽粉,它可以從undo log中分析出該行記錄以前的數(shù)據(jù)是什么,從而提供該行版本信息抓艳,幫助用戶實現(xiàn)一致性非鎖定讀取触机。我們舉一個具體的例子。例子來自此文玷或。
這個例子主要演示事務對某行記錄的更新過程:
在演示之前儡首,補充一下:
InnoDB為每行記錄都實現(xiàn)了三個隱藏字段,用來實現(xiàn)MVCC:
- 6字節(jié)的事務ID(DB_TRX_ID ,每處理一個事務偏友,其值自動+1椒舵。
- 7字節(jié)的回滾指針(DB_ROLL_PTR),指向?qū)懙絩ollback segment(回滾段)的一條undo log記錄约谈。
- 隱藏的ID
1. 初始數(shù)據(jù)行
F1~F6是某行列的名字,1~6是其對應的數(shù)據(jù)犁钟。后面三個隱含字段分別對應該行的事務號和回滾指針棱诱,假如這條數(shù)據(jù)是剛INSERT的,可以認為ID為1涝动,其他兩個字段為空迈勋。
2.事務1更改該行的各字段的值
當事務1更改該行的值時,會進行如下操作:
- 用排他鎖鎖定該行
- 記錄redo log
- 把該行修改前的值Copy(可以理解成Copy醋粟,不要糾結前面說反向更新這里說復制靡菇,原理是一樣的)到undo log重归,即上圖中下面的行
- 修改當前行的值,填寫事務編號厦凤,使回滾指針指向undo log中的修改前的行鼻吮。
3.事務2修改該行的值
與事務1相同,此時undo log较鼓,中有有兩行記錄椎木,并且通過回滾指針連在一起。
這些通過回滾指針聯(lián)系起來的行相當于是數(shù)據(jù)的多個快照博烂,從而實現(xiàn)MVCC的一致性非鎖定讀了香椎。
具體規(guī)則如下:
InnoDB的MVCC,是通過上面我們說的每行紀錄后面隱藏的列來實現(xiàn)的禽篱。他們保存了行的創(chuàng)建時間和行的過期時間(或刪除時間)畜伐,當然存儲的并不是實際的時間值,而是系統(tǒng)版本號躺率。每開始一個新的事務玛界,系統(tǒng)版本號都會自動遞增。事務開始時刻的系統(tǒng)版本號會作為事務的版本號肥照,用來和查詢到的每行紀錄的版本號進行比較脚仔。在REPEATABLE READ隔離級別下,MVCC具體的操作如下:
SELECT
InnoDB會根據(jù)以下兩個條件檢查每行紀錄:
- InnoDB只查找版本早于當前事務版本的數(shù)據(jù)行舆绎,即鲤脏,行的系統(tǒng)版本號小于或等于事務的系統(tǒng)版本號,這樣可以確保事務讀取的行吕朵,要么是在事務開始前已經(jīng)存在的猎醇,要么是事務自身插入或者修改過的。
- 行的刪除版本努溃,要么未定義硫嘶,要么大于當前事務版本號。這樣可以確保事務讀取到的行梧税,在事務開始之前未被刪除沦疾。
只有符合上述兩個條件的紀錄,才能作為查詢結果返回第队。
INSERT
- InnoDB為插入的每一行保存當前系統(tǒng)版本號作為行版本號哮塞。
DELETE
- InnoDB為刪除的每一行保存當前系統(tǒng)版本號作為行刪除標識。
UPDATE
- InnoDB為插入一行新紀錄凳谦,保存當前系統(tǒng)版本號作為行版本號忆畅,同時,保存當前系統(tǒng)版本號到原來的行作為行刪除標識尸执。
讀到這里家凯,也許會有一個疑問缓醋,考慮如下執(zhí)行序列:
按照之前的Select規(guī)則,會話B 的事務是在 會話A的后面開啟的绊诲,那么B的事務版本號大于A的事務版本號送粱。這樣在A中插入的數(shù)據(jù)在未提交的情況下,B可以讀到A修改的數(shù)據(jù)驯镊,這不就自相矛盾了么葫督?
其實不是,InnoDB通過read view來確定一致性讀時的數(shù)據(jù)庫snapshot,InnoDB的read view確定一條記錄能否看到,有兩條法則 :
1 看不到read view創(chuàng)建時刻以后啟動的事務
2 看不到read view創(chuàng)建時活躍的事務
對于Session A板惑,start transaction時并沒有創(chuàng)建read view橄镜,而是在update語句才創(chuàng)建。所以Session A 的read view創(chuàng)建時間要比Session B的晚冯乘。所以B是不會看到A的操作的洽胶。因此防止了不可重復讀。
兩條法則原文描述如下:
Rule 1: When the read view object is created it notes down the smallest transaction identifier that is not yet used as a transaction identifier (trx_sys_t::max_trx_id). The read view calls it the low limit. So the transaction using the read view must not see any transaction with identifier greater than or equal to this low limit.Rule 2: The transaction using the read view must not see a transaction that was active when the read view was created.
補充:如果undo log一直不刪除裆馒,則會通過當前記錄的回滾指針回溯到該行創(chuàng)建時的初始內(nèi)容姊氓,所幸的時在Innodb中存在purge線程,它會查詢那些比現(xiàn)在最老的活動事務還早的undo log喷好,并刪除它們翔横,從而保證undo log文件不至于無限增長。
關注公眾號: “Java不睡覺”梗搅, 回復:“資源”禾唁。獲取大數(shù)據(jù)全套視頻和大量Java書籍