一、事務概念
事務是一組操作的集合,他是一個不可分割的工作單位,事務會把所有操作作為一個整體一起向系統提交或者撤銷請求操作,即這些操作要麼同時成功,要麼同時失敗。
二、事務特性
- 原子性(Atomicity):事務是不可分割的最小操作單元,要麼全部成功,要麼全部失敗
- 一致性(Consistency):事務完成時,必須是所有的資料保持一致性狀態
- 隔離性(Isolation):資料庫系統提供的隔離機制,保證事務在不受外部併發操作影響的獨立環境下執行
- 永續性(Durability):事務一旦提交或回滾,他對資料庫中的資料的該表就是永久的
三、事務的隔離級別
- 讀未提交(Read Uncommitted):最低的隔離級別,事務可以讀取到其他事務尚未提交的資料,存在髒讀(Dirty Read)問題,即讀取到未提交的資料,可能導致資料的不一致性。
- 讀已提交(Read Committed):事務只能讀取到其他事務已經提交的資料,解決了髒讀的問題,但是可能導致不可重複讀(Non-Repeatable Read)問題,即在同一事物中多次讀取同一資料時可能會得到不同的結果。
- 可重複讀(Repeatable Read):可重複讀是MySQL預設的隔離級別,並且解決了不可重複讀問題,在一個事務中,多次讀取同一資料會得到相同的結果,及時其他事務更改了資料。
- 序列化讀(Serializable):最高的隔離級別,強制所有事務按照順序依次執行,避免了髒讀,不可重讀和幻讀(Phantom Read),幻讀指在同一事務中,多次查詢同一個範圍的資料時,結果集合的行數可能不一致。
四、Redo Log
在MySQL中,Redo Log和Undo Log是用來支援事務和保證資料一致性的關鍵日誌機制。
Redo Log(重做日誌): 作用是記錄了所有對資料庫的修改操作,包括插入、更新和刪除等操作。記錄了事務提交時資料頁的物理修改,是用來實現事務的永續性
該日誌檔案有兩部分組成:重做日誌緩衝(Redo Log Buffer)以及重做日誌檔案(Redo Log File),前者是在記憶體中,後者在磁碟中,當事務提交之後會把所有資訊都存到該日誌檔案中,使用者重新整理髒頁到磁碟,發生錯誤時,進行資料恢復使用。
工作原理:當事務進行資料修改時,MySQL將修改的操作記錄到 Redo Log中,而不直接寫入磁碟的資料檔案中。這樣可以減少磁碟IO的操作,提高效能。在事務提交時,Redo Log的內容會被非同步重新整理到磁碟上的資料檔案中,確保資料的永續性。如果系統崩潰,MySQL可以透過Redo Log中的資訊重做之前未寫入磁碟的修改操作,恢復到事務提交的狀態。
五、Undo Log
5.1 簡介
Undo Log(回滾日誌):用來支援事務的回滾操作,即將事務的修改操作撤銷,恢復到之前的狀態。在insert、update、delete的時候產生的便於資料回滾的日誌。
當insert的時候,產生的 Undo Log日誌只在回滾時需要,在事務提交後,可被立即刪除。
而update、delete的時候,產生的Undo Log日誌不僅在回滾時需要,在快照讀時也需要,不會立即被刪除。
工作原理:當事務進行資料修改時,MySQL將修改的操作記錄到Undo Log中,並在事務提交之前保留這些修改的記錄。如果事務發生回滾操作,MySQL會根據Undo Log中的資訊,將事務的修改操作撤銷,將資料還原到事務開始的狀態。因此,Undo Log為事務提供了撤銷操作的能力,確保資料庫的一致性。
5.2 Undo Log 版本鏈
不同事務或相同事務對同一條記錄進行修改,會導致該記錄的Undo Log生成一條記錄版本鏈,連結串列的頭部是最新的舊記錄,連結串列尾部是最早的舊記錄
說明1:第一次insert的資料,此時DB_TRX_ID 為1, 因為還沒有修改過,所以DB_ROLL_PTR為null
說明2:此時我們模擬幾個併發同時開始的事務
說明3:此時我們在事務2中修改id為30的資料
說明4:那麼在執行修改操作之前,會先把原始資料寫入Undo Log日誌中一份,方便資料回滾
說明5:在此時原始記錄中age改為3,並且隱藏欄位DB_TRX_ID變為2,DB_ROLL_PTR在為Undo Log 日誌中的第一條資料的地址
說明6:此時事務2提交事務之後,事務3又修改id為30的資料,name改為A3
說明7:此時在更改原始資料之前,需要把當前的資料在寫入到Undo Log日誌中一份,繼續方便做資料回滾,而這時Undo Log中就有了兩條資料
說明8:然後就可以修改原始資料 name 改為 A3,並且DB_TRX_ID也改為3,DB_ROLL_PTR指向Undo Log第二條日誌的地址。
說明9:這樣就形成了一條完整的Undo Log版本鏈
六、MVCC
6.1 當前讀
讀取的是記錄的最新版本,讀取時還要保證其他併發事務不能修改當前記錄,會對讀取的記錄進行枷鎖。
如:select...lock in share mode(共享鎖)、select...for update、update、insert、delete(排他鎖)都是一種當前讀。
6.2 快照讀
簡單的select(不加鎖)就是快照讀,快照讀讀取的是記錄資料的可見版本,有可能是歷史資料,不加鎖,是非阻塞讀。
-
- Read Committed:每次select,都生成一個快照讀。
- Repeatable Read:開啟事務後第一個select語句就會生成一個快照讀,其後面的select讀取的都是快照中的資料,所以資料才是可重複讀的。
- Serializable:快照讀會退化為當前讀
6.3 MVCC
全稱 Multi-Version Concurrency Control 多版本併發控制。指維護一個資料的多個版本,使得讀寫操作沒有衝突,快照讀為MySQL實現MVCC提供了一個非阻塞讀功能。MVCC具體實現,還需要依賴於資料庫記錄中的三個隱式欄位、Undo Log日誌、readView。
6.4 記錄中的三個隱藏欄位
示例如下:
DB_TRX_ID:最近修改事務ID,記錄插入這條記錄或最後一次修改該記錄的事務ID
DB_ROLL_PTR:回滾指標,指向這條記錄的上一個版本,用於配合Undo Log,指向上一個版本
DB_ROW_ID:隱式主鍵,如果表結構沒有指定主鍵,將會生成該隱藏欄位
使用指令重新建立一個表mvcc_test,這個時候不給該表設定主鍵
然後在資料庫路徑下的data資料夾下就可以找到我們剛建立的獨立表結構空間mvcc_test.ibd
MySQL提供了一個ibd2sdi 指令可以檢視錶結構空間
root@ubuntu:/usr/local/mysql/data/mysql_test# ibd2sdi mvcc_test.ibd ["ibd2sdi", { ... # 這裡的 ... 是我過濾掉的無關資料 "dd_object_type": "Table", # 這裡說明這是一個表結構 "dd_object": { "name": "mvcc_test", # 名稱 mvcc_test ... "columns": [ # 所有的列 { "name": "name", # 自己定義的name列 ... }, { "name": "DB_ROW_ID", # 隱式建立的列 ... }, { "name": "DB_TRX_ID", # 隱式建立的列,如果在建立表的時候指定了主鍵,則該隱藏欄位就不會被建立 ... }, { "name": "DB_ROLL_PTR", # 隱式建立的列 ... } ], "schema_ref": "mysql_test", # 該表所屬的資料庫 ... }
} ] root@ubuntu:/usr/local/mysql/data/mysql_test#
七、readView
7.1 readView(讀檢視)
是快照讀SQL執行時MVCC提取資料的依據,記錄並維護系統當前活躍的事務id,即未提交的事務id
7.2 readView中的四個核心欄位
-
- m_ids:當前活躍的事務ID集合,即當前未提交的事務的ID集合。
- min_trx_id:最小活躍事務ID。
- max_trx_id:預分配事務ID,當前最大事務ID+1,因為事務ID是自增的。
- creator_trx_id:readView建立者的事務ID。
7.3 版本鏈資料訪問規則
-
- 當前事務ID == creator_trx_id 可以訪問該版本:因為當前事務的id=creator_trx_id說明這個版本就是當前事務建立的。
- 當前事務ID < min_trx_id 可以訪問該版本:因為當前事務ID小於當前正在活躍事務的最小ID,說明當前事務已經提交了,並且該版本是在當前時候提交後才建立,所以可以訪問。
- 當前事務ID > max_trx_id 不可以訪問該版本:因為當前事務ID比readView最大的活躍ID還大,說明當前事務是在readVIew建立之後才開啟的,所以不能訪問以前的還未提交事務的資料。
- 當前事務ID >= min_trx_id 並且 <= max_trx_id 並且不在m_ids中 可以訪問該版本:因為不在m_ids中說明資料已經提交了,並且<= max_trx_id 又說明不是在readView建立之後開啟的,所以可以訪問。
7.4 不同隔離級別,生成readView的時機不同
-
- read committed:在事務中每一次執行快照讀時生成readView。
- repeatable read:僅在事務中第一次執行快照讀時生成readView,後續複用該readView。
7.5 read committed隔離級別下readView查詢資料的機制
因為read committed 模式下, 在事務中每一次執行快照讀時都會生成readView.所以在同一個事務下,讀取兩次資料,就會產生兩個ReadVIew
說明1:以事務5的兩次查詢為例
說明2:ReadView1中的m_ids:[3,4,5],因為事務2在開啟該次查詢事務的事就已經提交了,所以m_ids中不包括2,同樣的在ReadView2建立的時候,事務3也已經提交,所以ReadView2中的m_ids只有[4,5]
說明3:套用版本鏈訪問資料的規則,ReadView1和ReadView2兩次查詢資料演示
說明4:ReadView1資料查詢演示
- 使用DB_TRX_ID=4 的資料進行驗證,4 不等於 creator_trx_id(5),所以第一個原則不滿足
- 使用DB_TRX_ID=4 的資料進行驗證,4 不小於 min_trx_id(3), 所以第二個原則也不滿足
- 使用DB_TRX_ID=4 的資料進行驗證,4 不大於 max_tr_id(6), 滿足第三個原則,但是第三個原則是判斷不能訪問的,就算著這裡滿足也不一定能訪問,還要看第四個原則
- 使用DB_TRX_ID=4 的資料進行驗證,4 滿足 >=3 並且 <=6 , 但是4在m_ids[3,4,5]中,所以ReadVIew1不能使用DB_TRX_ID = 4 的資料,透過表格也可以看出開在ReadView1查詢的時候,事務4並沒有提交呢
- 使用DB_TRX_ID=3 的資料進行驗證,3 不等於 creator_trx_id(5),所以第一個原則不滿足
- 使用DB_TRX_ID=3 的資料進行驗證,3 不小於 min_trx_id(3), 所以第二個原則也不滿足
- 使用DB_TRX_ID=3 的資料進行驗證,3 不大於 max_tr_id(6), 滿足第三個原則,但是第三個原則是判斷不能訪問的,就算著這裡滿足也不一定能訪問,還要看第四個原則
- 使用DB_TRX_ID=3 的資料進行驗證,3 滿足 >=3 並且 <=6 , 但是3在m_ids[3,4,5]中,所以ReadVIew1不能使用DB_TRX_ID = 3 的資料,透過表格也可以看出開在ReadView1查詢的時候,事務3並沒有提交呢
- 使用DB_TRX_ID=2 的資料進行驗證,2 不等於 creator_trx_id(5),所以第一個原則不滿足
- 使用DB_TRX_ID=2 的資料進行驗證,2 小於 min_trx_id(3), 所以第二個原則滿足,則可以判斷ReadView1使用的是 DB_TRX_ID = 2的資料,即id=30, age=3,name=A30。
說明5:ReadView2資料查詢演示
- 使用DB_TRX_ID=4 的資料進行驗證,4 不等於 creator_trx_id(5),所以第一個原則不滿足
- 使用DB_TRX_ID=4 的資料進行驗證,4 不小於 min_trx_id(4), 所以第二個原則也不滿足
- 使用DB_TRX_ID=4 的資料進行驗證,4 不大於 max_tr_id(6), 滿足第三個原則,但是第三個原則是判斷不能訪問的,就算著這裡滿足也不一定能訪問,還要看第四個原則
- 使用DB_TRX_ID=4 的資料進行驗證,4 滿足 >=4 並且 <=6 , 但是4在m_ids[4,5]中,所以ReadVIew2不能使用DB_TRX_ID = 4 的資料,透過表格也可以看出開在ReadView2查詢的時候,事務4並沒有提交呢
- 使用DB_TRX_ID=3 的資料進行驗證,3 不等於 creator_trx_id(5),所以第一個原則不滿足
- 使用DB_TRX_ID=3 的資料進行驗證,3 不小於 min_trx_id(4), 所以第二個原則滿足,則可以判斷ReadView2使用的是 DB_TRX_ID = 3的資料,即id=30, age=3,name=A3。
7.6 repeatable read 隔離級別下readView查詢資料的機制
repeatable read:僅在事務中第一次執行快照讀時生成readView,後續複用該readView,所以這裡只會產生一個ReadVIew
說明6:因為這裡的資料查詢演示過程個上面的一樣,只不過這裡只有一個ReadVIew,過程更簡單。
八、事務的實現原理
8.1 MVCC的實現原理
三個隱藏欄位+Undo Log版本鏈+ReadView是MVCC的實現原理
8.2 事務隔離性的實現原理
MVCC+鎖就是事務隔離性的實現原理
8.3 事務原子性、一致性實現原理
Undo Log + Read Log
8.4 事務的永續性實現原理
Read Log