事務基礎
- 事務
事務 是一組操作的集合,它是一個不可分割的作業單位,事務會把所有的操作作為一個整體一起向系統提交或撤銷操作請求,即這些操作要么同時成功,要么同時失敗,
- 特性
- 原子性(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保證,

本文由
傳智教育博學谷狂野架構師教研團隊發布,如果本文對您有幫助,歡迎
關注和點贊;如果您有任何建議也可留言評論或私信,您的支持是我堅持創作的動力,轉載請注明出處!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/546800.html
標籤:其他
上一篇:Spring筆記_mybatis
下一篇:Java并發小結01
