捲起來!!!看了這篇文章我才知道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內存模型
- 時隔多年,這次我終於把動態代理的源碼翻了個地兒朝天
- 再有人問你分佈式事務,把這篇文章砸過去給他