undo log基礎
大家都知道,數據庫的四個隔離級別。有一個情況大家也熟悉:即RC和RR兩種隔離級別下的不同可見性,即不可重複讀問題。
不可重複讀的含義是事務A多次讀取同一數據,事務B在事務A多次讀取的過程中,對數據做了更新並提交,導致事務A多次讀取時數據不一致
在RC隔離級別下,僞代碼
session1
start transaction;
session2
start transaction;
session1先讀取一次,是1200
session2加了300,之後commit
session1再讀取一次,是1500
如果session1基於1200進行了操作,就可能造成數據紊亂的結果
而在RR隔離級別下,結果
會發現session1讀取的結果一致都是第一次start transaction之前數據的值,在整個session過程中不變,比如說都是1200
而在RR隔離級別下,如果我就在這個基礎上做修改,會存在問題嗎?
session2 1500
session1 read 仍是1200,但其執行
UPDATE account_innodb SET balance = balance - 100 WHERE id = 1;
commit;
再查詢,結果是1400,是正確的,而不是我們之前預想的1100
這個不可重複讀的問題,或者說是RC、RR下innodb的快照讀/非阻塞讀是如何實現的呢?
答案就是由undo log來提供支持的。
undo log用來實現事務的一致性,即事務ACID中的C–consistence,支持回滾和MVCC多版本控制
undo log是記錄到undo page中的,默認存放在 ibdata1中,即系統表空間中。表空間內部由多個segment段對象(邏輯概念)組成,每個段由extend區(邏輯概念)組成,每個區由頁(物理概念)組成,在每個頁中保存數據。
undo log的工作方式
-
背景知識1: mysql數據行裏的DB_TRX_ID, DB_ROLL_PTR, DB_ROW_ID字段
- DB_TRX_ID字段標識最近一次對本行記錄做修改,insert或update的事務id,至於說delete操作,在innodb看來,也不過是一次update操作,有一個deleted的隱藏列
- DB_ROLL_PTR (rollback pointer),即回滾指針,指寫入回滾段rollback segment的undo日誌記錄,如果一行記錄被更新,則undo log record包含重建該行記錄被更新之前內容所必須的信息
- DB_ROW_ID(當innodb引擎沒有任何索引時,會自動創建的隱藏的主鍵列)包含一個隨着新行插入而單調遞增的行ID,當由innodb自動產生聚集索引時,聚集索引會包括行ID的值,否則這個行ID不會出現在任何索引中
-
背景知識2: undo日誌
當我們對記錄做了變更操作時,就會產生undo日誌。
undo日誌中存儲的是老版數據,當一箇舊的事務要去讀取數據時,爲了能夠讀取到老版本的數據,需要順着undo列找到滿足其可見性的記錄。
undo log主要分爲兩種:insert undo log和update undo log。
其中,insert undo log表示的是事務對insert新紀錄產生的undo log,只在事務回滾時需要,並且可以在事務提交時就可以丟棄,其不是講解的重點。
重點是update undo log,事務對記錄進行update或者delete操作時會產生update undo log,不僅在事務回滾時需要,快照讀也需要,所以不能隨便刪除,只有當數據庫所使用的快照中不涉及該日誌記錄,對應的回滾日誌纔會被purge線程刪除。
undo log日誌的工作方式簡化的演示,這裏只是顯示了事務對行記錄的更新過程(innodb在內部做了非常多的工作)
上圖表示我們對DB_ROW_ID=1的行做了update,這一行被事務A做了修改,將原來field2裏面的值由12更新爲了32。
其修改的過程是這樣的:
-
首先用排他鎖鎖定該行
-
然後把該行修改前的值copy一份到undo log裏面
-
之後修改當前行的值,填寫事務id到DB_TRX_ID,使用DB_ROLL_PTR回滾指針指向undo log中修改前的行
在這之後,如果數據庫中還有別的事務再用快照讀來讀取該日誌記錄,那麼對應的undo log還沒有被清除,此時某個事務又對同一行記錄做了修改,將其fields3的值由13修改爲了45
這樣又會多了一條undo log記錄,數據的多個版本就是這樣實現的
以上就是undo log的大概樣子,它按照修改的時間順序從今到遠,通過DB_ROLL_PTR給連接起來了
RC、RR級別下的innodb的快照讀/非阻塞讀如何實現
首先是read view的概念,主要是用來做可見性判斷的,即當我們去執行快照讀select的時候,會針對我們查詢的數據創建出一個read view,來決定當前事務能看到的是哪個版本的數據。有可能是當前最新版本的數據,也有可能是undo log中某個版本的數據。
read view遵循一個可見性算法,主要是將要修改數據的DB_TRX_ID取出來,與系統其它活躍事務ID做對比,如果大於或者等於這些ID的話,就通過DB_ROLL_PTR指針去取出undo log上一層的DB_TRX_ID,直到小於這些活躍事務ID爲止,這樣就保證了我們當前取到的數據版本是當前可見的最穩定的版本。
mysql中的源碼
可以看到其有m_low_limit_id和m_up_limit_id
每當我們start transaction的時候,事務id都會遞增,也就是說越新開啓的事務,其id就越大。
我們主要就是通過這兩個值,去和我們的DB_TRX_ID做對比,進而決定讓他是不是去回溯到我們的undo log,去取出適應該版本的一個數據的版本來。
總結:正是因爲生成時機的不同,造成了RC、RR兩種隔離級別下的不同可見性。
在repeatable read級別下,session在start transaction後的第一條快照讀,會創建一個快照,即read view,將當前系統中活躍的其他事務記錄起來,此後再調用快照讀的時候,還是使用的同一個read view。而在read committed級別下,事務中每條select語句,每次調用快照讀的時候,都會創建一個新的快照,這就是爲什麼之前,我們在RC級別下,能用快照讀看到別的事務已經提交到的對錶記錄的增刪改。而在RR級別下,如果首次使用快照讀,是在別的事務對數據庫記錄進行增刪改並提交之前的,此後即便別的事務對記錄進行了增刪改並提交,還是讀不到數據的變動的原因。對RR來說,首次事務select的時機是相當重要的。
所以,在RC下可以看到兩次select的結果不同,而在RR下,都是讀取同一個快照,所以每次select的結果相同
由於undo log的支持,使得innodb在RC和RR級別下支持非阻塞讀,而讀取數據時的非阻塞就是所謂的MVCC。而innodb的非阻塞讀機制就實現了仿造版的MVCC。
MVCC就是讀不加鎖,讀寫不衝突,在讀多寫少的OLTP中,極大增加了系統的併發功能。爲什麼這裏只實現了僞MVCC功能呢?並沒有實現核心的多版本共存,undo log中的內容只是串行化的結果,記錄了多個事務的過程,不屬於多版本共存。
undo log物理存儲
undo log是記錄到undo page中的,默認存放在 ibdata1中,即系統表空間中。表空間內部由多個segment段對象(邏輯概念)組成,每個段由extend區(邏輯概念)組成,每個區由頁(物理概念)組成,在每個頁中保存數據。
rollback segment (回滾段)
-
MySQL 5.5前只有1個rollback segment
-
MySQL 5.5+ 有128個rollback segment
-
不保存任何undo log
-
僅保存undo log segment的位置
-
含有1024個undo slot
MySQL5.5中只有一個Rollback Segment,即只有1024個undo log segment,那就表示最多隻有能有1024個併發事務(線程)去執行undo
如果用不到undo ,其實是可以超過1024 個線程的。哪些線程會用到undo呢?事務下的增刪改會用到,在秒殺場景下是不是會有問題的?
在MySQL5.6中支持128 * 1024 個併發執行undo的線程
undo log segment(undo日誌段)
實際存儲undo log的對象
由undo page組成
每個undo page可以保存多個事務的undo log
最重要的是undo log中存儲了哪些內容
undo log header
undo log records – undo log記錄分爲兩種, insert 的undo和update 的undo
-
insert undo log record – 記錄insert
-
update undo log record – 記錄update和delete
undo log 是邏輯記錄
,記錄了每一行修改的值(前後項)。
undo log清理–purge線程
真正的刪除記錄
刪除undo log
舉例:表tb1 中有記錄pk=1,2,3;此時delete from tb1 where pk=1;
-
將pk=1的記錄標記爲刪除(delete-mark,info bits),數據庫中pk=1的記錄此時還是存在的,空間並沒有被釋放,該操作爲同步操作(SQL執行完,也就標記完成了)。
-
purge,該部分爲後臺線程(purge線程)異步操作,會真正的刪除該記錄,且空間被釋放。purge線程是系統自動的,無法人工控制。
標記爲已刪除的原因:
-
該事務可能需要回滾,先作保留。
-
當事務1 去刪除pk=1且沒有提交時, 事務2 應該要能看到pk=1的記錄(事務的隔離性)。
問題1:我們既然有了undo日誌,爲什麼還要delete-mark,然後purge呢?
在這裏,我們要區分幾種情況了
-
過濾條件是聚集索引
delete – 將該記錄標記爲delete-mark 。
update – 將該記錄先物理delete(聚簇索引裏主鍵相同的行最多只能有1個),然後insert或者可以原地更新[in place update](即使刪除了,也可以通過undo進行還原)。
-
過濾條件是二級索引
delete – 將該記錄標記爲delete-mark 。
update – 將該記錄標記爲delete-mark (索引列是columns + pk,即使是唯一索引更新也是和原來的不一樣),然後insert 。
問題2:爲什麼沒有insert
- insert操作是不需要異步去purge,因爲insert的記錄之前是不存在的
- 不存在記錄(未提交)是沒有別的事務能引用到的,所以insert以後,對應的undo可以直接刪除,而不需要等待異步purge
redo log與undo log區別
- redo log用來保證事務的原子性和持久性,undo log用來保證事務的一致性
- redo log和undo log都可以看做是一種恢復操作,redo恢復提交事務修改的頁操作,而undo回滾行記錄到某個特定版本,因此兩者記錄的內容不同。而且redo是物理邏輯日誌,根據頁進行記錄(物理),記錄的是頁的變化(邏輯),而undo log是邏輯日誌,根據每行記錄進行記錄