捲起來!!!看了這篇文章我才知道MySQL事務&MVCC到底是啥?
事務基礎
- 事務
事務 是一組操作的集合,它是一個不可分割的工作單位,事務會把所有的操作作為一個整體一起向系統提交或撤銷操作請求,即這些操作要麼同時成功,要麼同時失敗。
- 特性
- 原子性(Atomicity):事務是不可分割的最小操作單元,要麼全部成功,要麼全部失敗。
- 一致性(Consistency):事務完成時,必須使所有的資料都保持一致狀態。
- 隔離性(Isolation):資料庫系統提供的隔離機制,保證事務在不受外部併發操作影響的獨立環境下執行。
- 永續性(Durability):事務一旦提交或回滾,它對資料庫中的資料的改變就是永久的。
那實際上,我們研究事務的原理,就是研究MySQL的InnoDB引擎是如何保證事務的這四大特性的。
而對於這四大特性,實際上分為兩個部分。 其中的原子性、一致性、持久化,實際上是由InnoDB中的兩份日誌來保證的,一份是redo log日誌,一份是undo log日誌。 而永續性是通過資料庫的鎖,加上MVCC來保證的。
接下來主要就是來研究一下redolog,undolog以及MVCC。
redo log
重做日誌,記錄的是事務提交時資料頁的物理修改,是用來實現事務的永續性。
該日誌檔案由兩部分組成:重做日誌緩衝(redo log buffer)以及重做日誌檔案(redo log file),前者是在記憶體中,後者在磁碟中。當事務提交之後會把所有修改資訊都存到該日誌檔案中, 用於在重新整理髒頁到磁碟,發生錯誤時, 進行資料恢復使用。
如果沒有redolog,可能會存在什麼問題的? 我們一起來分析一下。
我們知道,在InnoDB引擎中的記憶體結構中,主要的記憶體區域就是緩衝池,在緩衝池中快取了很多的資料頁。 當我們在一個事務中,執行多個增刪改的操作時,InnoDB引擎會先操作緩衝池中的資料,如果緩衝區沒有對應的資料,會通過後臺執行緒將磁碟中的資料加載出來,存放在緩衝區中,然後將緩衝池中的資料修改,修改後的資料頁我們稱為髒頁。 而髒頁則會在一定的時機,通過後臺執行緒重新整理到磁碟中,從而保證緩衝區與磁碟的資料一致。 而緩衝區的髒頁資料並不是實時重新整理的,而是一段時間之後將緩衝區的資料重新整理到磁碟中,假如重新整理到磁碟的過程出錯了,而提示給使用者事務提交成功,而資料卻沒有持久化下來,這就出現問題了,沒有保證事務的永續性。
那麼,如何解決上述的問題呢? 在InnoDB中提供了一份日誌 redo log,接下來我們再來分析一下,通過redolog如何解決這個問題。
有了redolog之後,當對緩衝區的資料進行增刪改之後,會首先將操作的資料頁的變化,記錄在redo log buffer中。在事務提交時,會將redo log buffer中的資料重新整理到redo log磁碟檔案中。過一段時間之後,如果重新整理緩衝區的髒頁到磁碟時,發生錯誤,此時就可以藉助於redo log進行資料恢復,這樣就保證了事務的永續性。 而如果髒頁成功重新整理到磁碟 或 或者涉及到的資料已經落盤,此時redolog就沒有作用了,就可以刪除了,所以存在的兩個redolog檔案是迴圈寫的。
那為什麼每一次提交事務,要重新整理redo log 到磁碟中呢,而不是直接將buffer pool中的髒頁重新整理到磁碟呢 ?
因為在業務操作中,我們操作資料一般都是隨機讀寫磁碟的,而不是順序讀寫磁碟。 而redo log在往磁碟檔案中寫入資料,由於是日誌檔案,所以都是順序寫的。順序寫的效率,要遠大於隨機寫。 這種先寫日誌的方式,稱之為 WAL(Write-Ahead Logging)。
undo log
回滾日誌,用於記錄資料被修改前的資訊 , 作用包含兩個 : 提供回滾(保證事務的原子性) 和MVCC(多版本併發控制) 。
undo log和redo log記錄物理日誌不一樣,它是邏輯日誌。可以認為當delete一條記錄時,undolog中會記錄一條對應的insert記錄,反之亦然,當update一條記錄時,它記錄一條對應相反的update記錄。當執行rollback時,就可以從undo log中的邏輯記錄讀取到相應的內容並進行回滾。
Undo log銷燬:undo log在事務執行時產生,事務提交時,並不會立即刪除undo log,因為這些日誌可能還用於MVCC。
Undo log儲存:undo log採用段的方式進行管理和記錄,存放在前面介紹的 rollback segment回滾段中,內部包含1024個undo log segment。
MVCC
基本概念
- 當前讀
讀取的是記錄的最新版本,讀取時還要保證其他併發事務不能修改當前記錄,會對讀取的記錄進行加鎖。對於我們日常的操作,如:select ... lock in share mode
(共享鎖),select ...for update
、update
、insert
、delete
(排他鎖)都是一種當前讀。
測試:
在測試中我們可以看到,即使是在預設的RR隔離級別下,事務A中依然可以讀取到事務B最新提交的內容,因為在查詢語句後面加上了 lock in share mode
共享鎖,此時是當前讀操作。當然,當我們加排他鎖的時候,也是當前讀操作。
- 快照讀
簡單的select(不加鎖)就是快照讀,快照讀,讀取的是記錄資料的可見版本,有可能是歷史資料,不加鎖,是非阻塞讀。
- Read Committed:每次select,都生成一個快照讀。
- Repeatable Read:開啟事務後第一個select語句才是快照讀的地方。
- Serializable:快照讀會退化為當前讀。
測試:
在測試中,我們看到即使事務B提交了資料,事務A中也查詢不到。 原因就是因為普通的select是快照讀,而在當前預設的RR隔離級別下,開啟事務後第一個select語句才是快照讀的地方,後面執行相同的select語句都是從快照中獲取資料,可能不是當前的最新資料,這樣也就保證了可重複讀。
- MVCC
全稱 Multi-Version Concurrency Control,多版本併發控制。指維護一個數據的多個版本,使得讀寫操作沒有衝突,快照讀為MySQL實現MVCC提供了一個非阻塞讀功能。MVCC的具體實現,還需要依賴於資料庫記錄中的三個隱式欄位、undo log日誌、readView。
接下來,我們再來介紹一下InnoDB引擎的表中涉及到的隱藏欄位 、undolog 以及 readview,從而來介紹一下MVCC的原理。
隱藏欄位
介紹
當我們建立了上面的這張表,我們在查看錶結構的時候,就可以顯式的看到這三個欄位。 實際上除了這三個欄位以外,InnoDB還會自動的給我們新增三個隱藏欄位及其含義分別是:
隱藏欄位 | 含義 |
---|---|
DB_TRX_ID | 最近修改事務ID,記錄插入這條記錄或最後一次修改該記錄的事務ID。 |
DB_ROLL_PTR |
回滾指標,指向這條記錄的上一個版本,用於配合undo log,指向上一個版本。 |
DB_ROW_ID |
隱藏主鍵,如果表結構沒有指定主鍵,將會生成該隱藏欄位。 |
而上述的前兩個欄位是肯定會新增的, 是否新增最後一個欄位DB_ROW_ID,得看當前表有沒有主鍵,如果有主鍵,則不會新增該隱藏欄位。
測試
- 檢視有主鍵的表 stu
進入伺服器中的 /var/lib/mysql/MySQL_Advanced/ , 檢視stu的表結構資訊, 通過如下指令:
ibd2sdi stu.ibd
檢視到的表結構資訊中,有一欄 columns,在其中我們會看到處理我們建表時指定的欄位以外,還有額外的兩個欄位 分別是:DB_TRX_ID 、 DB_ROLL_PTR ,因為該表有主鍵,所以沒有DB_ROW_ID隱藏欄位。
{
"name": "DB_TRX_ID",
"type": 10,
"is_nullable": false,
"is_zerofill": false,
"is_unsigned": false,
"is_auto_increment": false,
"is_virtual": false,
"hidden": 2,
"ordinal_position": 4,
"char_length": 6,
"numeric_precision": 0,
"numeric_scale": 0,
"numeric_scale_null": true,
"datetime_precision": 0,
"datetime_precision_null": 1,
"has_no_default": false,
"default_value_null": true,
"srs_id_null": true,
"srs_id": 0,
"default_value": "",
"default_value_utf8_null": true,
"default_value_utf8": "",
"default_option": "",
"update_option": "",
"comment": "",
"generation_expression": "",
"generation_expression_utf8": "",
"options": "",
"se_private_data": "table_id=1074;",
"engine_attribute": "",
"secondary_engine_attribute": "",
"column_key": 1,
"column_type_utf8": "",
"elements": [],
"collation_id": 63,
"is_explicit_collation": false
},
{
"name": "DB_ROLL_PTR",
"type": 9,
"is_nullable": false,
"is_zerofill": false,
"is_unsigned": false,
"is_auto_increment": false,
"is_virtual": false,
"hidden": 2,
"ordinal_position": 5,
"char_length": 7,
"numeric_precision": 0,
"numeric_scale": 0,
"numeric_scale_null": true,
"datetime_precision": 0,
"datetime_precision_null": 1,
"has_no_default": false,
"default_value_null": true,
"srs_id_null": true,
"srs_id": 0,
"default_value": "",
"default_value_utf8_null": true,
"default_value_utf8": "",
"default_option": "",
"update_option": "",
"comment": "",
"generation_expression": "",
"generation_expression_utf8": "",
"options": "",
"se_private_data": "table_id=1074;",
"engine_attribute": "",
"secondary_engine_attribute": "",
"column_key": 1,
"column_type_utf8": "",
"elements": [],
"collation_id": 63,
"is_explicit_collation": false
}
- 檢視沒有主鍵的表 employee
建表語句:
create table employee (id int , name varchar(10));
此時,我們再通過以下指令來查看錶結構及其其中的欄位資訊:
ibd2sdi employee.ibd
檢視到的表結構資訊中,有一欄 columns,在其中我們會看到處理我們建表時指定的欄位以外,還有額外的三個欄位 分別是:DB_TRX_ID 、 DB_ROLL_PTR 、DB_ROW_ID,因為employee表是沒有指定主鍵的。
{
"name": "DB_ROW_ID",
"type": 10,
"is_nullable": false,
"is_zerofill": false,
"is_unsigned": false,
"is_auto_increment": false,
"is_virtual": false,
"hidden": 2,
"ordinal_position": 3,
"char_length": 6,
"numeric_precision": 0,
"numeric_scale": 0,
"numeric_scale_null": true,
"datetime_precision": 0,
"datetime_precision_null": 1,
"has_no_default": false,
"default_value_null": true,
"srs_id_null": true,
"srs_id": 0,
"default_value": "",
"default_value_utf8_null": true,
"default_value_utf8": "",
"default_option": "",
"update_option": "",
"comment": "",
"generation_expression": "",
"generation_expression_utf8": "",
"options": "",
"se_private_data": "table_id=1076;",
"engine_attribute": "",
"secondary_engine_attribute": "",
"column_key": 1,
"column_type_utf8": "",
"elements": [],
"collation_id": 63,
"is_explicit_collation": false
},
{
"name": "DB_TRX_ID",
"type": 10,
"is_nullable": false,
"is_zerofill": false,
"is_unsigned": false,
"is_auto_increment": false,
"is_virtual": false,
"hidden": 2,
"ordinal_position": 4,
"char_length": 6,
"numeric_precision": 0,
"numeric_scale": 0,
"numeric_scale_null": true,
"datetime_precision": 0,
"datetime_precision_null": 1,
"has_no_default": false,
"default_value_null": true,
"srs_id_null": true,
"srs_id": 0,
"default_value": "",
"default_value_utf8_null": true,
"default_value_utf8": "",
"default_option": "",
"update_option": "",
"comment": "",
"generation_expression": "",
"generation_expression_utf8": "",
"options": "",
"se_private_data": "table_id=1076;",
"engine_attribute": "",
"secondary_engine_attribute": "",
"column_key": 1,
"column_type_utf8": "",
"elements": [],
"collation_id": 63,
"is_explicit_collation": false
},
{
"name": "DB_ROLL_PTR",
"type": 9,
"is_nullable": false,
"is_zerofill": false,
"is_unsigned": false,
"is_auto_increment": false,
"is_virtual": false,
"hidden": 2,
"ordinal_position": 5,
"char_length": 7,
"numeric_precision": 0,
"numeric_scale": 0,
"numeric_scale_null": true,
"datetime_precision": 0,
"datetime_precision_null": 1,
"has_no_default": false,
"default_value_null": true,
"srs_id_null": true,
"srs_id": 0,
"default_value": "",
"default_value_utf8_null": true,
"default_value_utf8": "",
"default_option": "",
"update_option": "",
"comment": "",
"generation_expression": "",
"generation_expression_utf8": "",
"options": "",
"se_private_data": "table_id=1076;",
"engine_attribute": "",
"secondary_engine_attribute": "",
"column_key": 1,
"column_type_utf8": "",
"elements": [],
"collation_id": 63,
"is_explicit_collation": false
}
],
undolog
介紹
回滾日誌,在insert、update、delete的時候產生的便於資料回滾的日誌。
當insert的時候,產生的undo log日誌只在回滾時需要,在事務提交後,可被立即刪除。
而update、delete的時候,產生的undo log日誌不僅在回滾時需要,在快照讀時也需要,不會立即被刪除。
版本鏈
有一張表原始資料為:
DB_TRX_ID
: 代表最近修改事務ID,記錄插入這條記錄或最後一次修改該記錄的事務ID,是自增的。
DB_ROLL_PTR
: 由於這條資料是才插入的,沒有被更新過,所以該欄位值為null。
然後,有四個併發事務同時在訪問這張表。
A. 第一步
當事務2執行第一條修改語句時,會記錄undo log日誌,記錄資料變更之前的樣子; 然後更新記錄,並且記錄本次操作的事務ID,回滾指標,回滾指標用來指定如果發生回滾,回滾到哪一個版本。
B.第二步
當事務3執行第一條修改語句時,也會記錄undo log日誌,記錄資料變更之前的樣子; 然後更新記錄,並且記錄本次操作的事務ID,回滾指標,回滾指標用來指定如果發生回滾,回滾到哪一個版本。
C. 第三步
當事務4執行第一條修改語句時,也會記錄undo log日誌,記錄資料變更之前的樣子; 然後更新記錄,並且記錄本次操作的事務ID,回滾指標,回滾指標用來指定如果發生回滾,回滾到哪一個版本。
最終我們發現,不同事務或相同事務對同一條記錄進行修改,會導致該記錄的undolog生成一條記錄版本連結串列,連結串列的頭部是最新的舊記錄,連結串列尾部是最早的舊記錄。
readview
ReadView(讀檢視)是 快照讀 SQL執行時MVCC提取資料的依據,記錄並維護系統當前活躍的事務(未提交的)id。
ReadView中包含了四個核心欄位:
欄位 | 含義 |
---|---|
m_ids | 當前活躍的事務ID集合 |
min_trx_id | 最小活躍事務ID |
max_trx_id | 預分配事務ID,當前最大事務ID+1(因為事務ID是自增的) |
creator_trx_id | ReadView建立者的事務ID |
而在readview中就規定了版本鏈資料的訪問規則:
trx_id 代表當前undolog版本鏈對應事務ID。
條件 | 是否可以訪問 | 說明 |
---|---|---|
trx_id == creator_trx_id | 可以訪問該版本 | 成立,說明資料是當前這個事務更改的。 |
trx_id < min_trx_id | 可以訪問該版本 | 成立,說明資料已經提交了。 |
trx_id > max_trx_id | 不可以訪問該版本 | 成立,說明該事務是在ReadView生成後才開啟。 |
min_trx_id <= trx_id <= max_trx_id | 如果trx_id不在m_ids中,是可以訪問該版本的 | 成立,說明資料已經提交。 |
不同的隔離級別,生成ReadView的時機不同:
- READ COMMITTED :在事務中每一次執行快照讀時生成ReadView。
- REPEATABLE READ:僅在事務中第一次執行快照讀時生成ReadView,後續複用該ReadView。
原理分析
RC隔離級別
RC隔離級別下,在事務中每一次執行快照讀時生成ReadView。
我們就來分析事務5中,兩次快照讀讀取資料,是如何獲取資料的?
在事務5中,查詢了兩次id為30的記錄,由於隔離級別為Read Committed,所以每一次進行快照讀都會生成一個ReadView,那麼兩次生成的ReadView如下。
那麼這兩次快照讀在獲取資料時,就需要根據所生成的ReadView以及ReadView的版本鏈訪問規則,到undolog版本鏈中匹配資料,最終決定此次快照讀返回的資料。
A. 先來看第一次快照讀具體的讀取過程:
在進行匹配時,會從undo log的版本鏈,從上到下進行挨個匹配:
- 先匹配
這條記錄,這條記錄對應的trx_id為4,也就是將4帶入右側的匹配規則中。 ①不滿足 ②不滿足 ③不滿足 ④也不滿足 ,都不滿足,則繼續匹配undo log版本鏈的下一條。
- 再匹配第二條
,這條記錄對應的trx_id為3,也就是將3帶入右側的匹配規則中。①不滿足 ②不滿足 ③不滿足 ④也不滿足 ,都不滿足,則繼續匹配undo log版本鏈的下一條。
- 再匹配第三條
,這條記錄對應的trx_id為2,也就是將2帶入右側的匹配規則中。①不滿足 ②滿足 終止匹配,此次快照讀,返回的資料就是版本鏈中記錄的這條資料。
B. 再來看第二次快照讀具體的讀取過程:
在進行匹配時,會從undo log的版本鏈,從上到下進行挨個匹配:
- 先匹配
這條記錄,這條記錄對應的trx_id為4,也就是將4帶入右側的匹配規則中。 ①不滿足 ②不滿足 ③不滿足 ④也不滿足 ,都不滿足,則繼續匹配undo log版本鏈的下一條。
- 再匹配第二條
,這條記錄對應的trx_id為3,也就是將3帶入右側的匹配規則中。①不滿足 ②滿足 。終止匹配,此次快照讀,返回的資料就是版本鏈中記錄的這條資料。
RR隔離級別
RR隔離級別下,僅在事務中第一次執行快照讀時生成ReadView,後續複用該ReadView。 而RR 是可重複讀,在一個事務中,執行兩次相同的select語句,查詢到的結果是一樣的。
那MySQL是如何做到可重複讀的呢? 我們簡單分析一下就知道了
我們看到,在RR隔離級別下,只是在事務中第一次快照讀時生成ReadView,後續都是複用該ReadView,那麼既然ReadView都一樣, ReadView的版本鏈匹配規則也一樣, 那麼最終快照讀返回的結果也是一樣的。
所以呢,MVCC的實現原理就是通過 InnoDB表的隱藏欄位、UndoLog 版本鏈、ReadView來實現的。而MVCC + 鎖,則實現了事務的隔離性。 而一致性則是由redolog 與 undolog保證。
本文由
傳智教育博學谷狂野架構師
教研團隊釋出。如果本文對您有幫助,歡迎
關注
和點贊
;如果您有任何建議也可留言評論
或私信
,您的支援是我堅持創作的動力。轉載請註明出處!
- ElasticSearch還能效能調優,漲見識、漲見識了!!!
- 【必須收藏】別再亂找TiDB 叢集部署教程了,這篇保姆級教程來幫你!!| 博學谷狂野架構師
- 【建議收藏】7000 字的TIDB保姆級簡介,你見過嗎
- Tomcat架構設計剖析 | 博學谷狂野架構師
- 你可能不那麼知道的Tomcat生命週期管理 | 博學谷狂野架構師
- 大哥,這是併發不是並行,Are You Ok?
- 為啥要重學Tomcat?| 博學谷狂野架構師
- 這是一篇純講SQL語句優化的文章!!!| 博學谷狂野架構師
- 捲起來!!!看了這篇文章我才知道MySQL事務&MVCC到底是啥?
- 為什麼99%的程式設計師都做不好SQL優化?
- 如何搞定MySQL鎖(全域性鎖、表級鎖、行級鎖)?這篇文章告訴你答案!太TMD詳細了!!!
- 【建議收藏】超詳細的Canal入門,看這篇就夠了!!!
- 從菜鳥程式設計師到高階架構師,竟然是因為這個字final
- 為什麼95%的Java程式設計師,都是用不好Synchronized?
- 99%的Java程式設計師者,都敗給這一個字!
- 8000 字,就說一個字Volatile
- 98%的程式設計師,都沒有研究過JVM重排序和順序一致性
- 來一波騷操作,Java記憶體模型
- 時隔多年,這次我終於把動態代理的原始碼翻了個地兒朝天
- 再有人問你分散式事務,把這篇文章砸過去給他