mysql undo log研究

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. 背景知識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. 背景知識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。

其修改的過程是這樣的:

  1. 首先用排他鎖鎖定該行

  2. 然後把該行修改前的值copy一份到undo log裏面

  3. 之後修改當前行的值,填寫事務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 (回滾段)

  1. MySQL 5.5前只有1個rollback segment

  2. MySQL 5.5+ 有128個rollback segment

  3. 不保存任何undo log

  4. 僅保存undo log segment的位置

  5. 含有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

  1. insert undo log record – 記錄insert

  2. update undo log record – 記錄update和delete

undo log 是邏輯記錄,記錄了每一行修改的值(前後項)。

undo log清理–purge線程

真正的刪除記錄

刪除undo log

舉例:表tb1 中有記錄pk=1,2,3;此時delete from tb1 where pk=1;

  1. 將pk=1的記錄標記爲刪除(delete-mark,info bits),數據庫中pk=1的記錄此時還是存在的,空間並沒有被釋放,該操作爲同步操作(SQL執行完,也就標記完成了)。

  2. purge,該部分爲後臺線程(purge線程)異步操作,會真正的刪除該記錄,且空間被釋放。purge線程是系統自動的,無法人工控制。

標記爲已刪除的原因:

  1. 該事務可能需要回滾,先作保留。

  2. 當事務1 去刪除pk=1且沒有提交時, 事務2 應該要能看到pk=1的記錄(事務的隔離性)。

問題1:我們既然有了undo日誌,爲什麼還要delete-mark,然後purge呢?

在這裏,我們要區分幾種情況了

  1. 過濾條件是聚集索引

    delete – 將該記錄標記爲delete-mark 。

    update – 將該記錄先物理delete(聚簇索引裏主鍵相同的行最多只能有1個),然後insert或者可以原地更新[in place update](即使刪除了,也可以通過undo進行還原)。

  2. 過濾條件是二級索引

    delete – 將該記錄標記爲delete-mark 。

    update – 將該記錄標記爲delete-mark (索引列是columns + pk,即使是唯一索引更新也是和原來的不一樣),然後insert 。

問題2:爲什麼沒有insert

  1. insert操作是不需要異步去purge,因爲insert的記錄之前是不存在的
  2. 不存在記錄(未提交)是沒有別的事務能引用到的,所以insert以後,對應的undo可以直接刪除,而不需要等待異步purge

redo log與undo log區別

  1. redo log用來保證事務的原子性和持久性,undo log用來保證事務的一致性
  2. redo log和undo log都可以看做是一種恢復操作,redo恢復提交事務修改的頁操作,而undo回滾行記錄到某個特定版本,因此兩者記錄的內容不同。而且redo是物理邏輯日誌,根據頁進行記錄(物理),記錄的是頁的變化(邏輯),而undo log是邏輯日誌,根據每行記錄進行記錄
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章