iT邦幫忙

2023 iThome 鐵人賽

DAY 7
1
Software Development

CRUD仔的一生(上集)系列 第 8

[ACID] 樂觀鎖(MVCC In MySQL)

  • 分享至 

  • xImage
  •  

Mysql MVCC 原理

前言

前面介紹了樂觀鎖的原理,這裡將介紹在 Mysql 中,是如何利用樂觀鎖的原理來建構 MVCC DB 的。

基本介紹

在理解 mysql mvcc 時,這裡會有些不一樣的地方
mvcc 的重點實作在於當某筆資料要被 read 時,
隨時可以被 Read, 不會因為某 Tx 在 write 後尚未 commit 而卡住不可讀。

因為 mysql 的 isolation level 是透過 sxlock 實作的,
所以他的 read/write 是真的會對紀錄上鎖的。
又因為是無法直接採用增加類似 version 欄位來實作樂觀鎖就算加上也會因 lock 住而無法讀取,
所以不能像是 postgres 透過一些隱藏欄位+clog 的方式來實作 isolation level 。

sxlock 的 query 問題

我們知道 Mysql 是透過 sxlock 來做 isolation level,先假設 mysql 沒有實作 mvcc 時的樣子

time tx1 tx2
1 begin
2 select ... where id=1
3 begin
4 update ... where id=1
5 select ... where id=1 block
6

只要做過 write,後面不管是 read/write 都會被 block 住,
我們稱為 read-write conflict,
為了使用更好的方式解決 read-write conflict ,
因此 mysql 時做出 read view 的概念

ReadView

意思就是在 read 時,建立一張快照(snapshot),就如同當有人在寫檔案時,你至少可以唯讀的概念類似。
而 read 是拿那張快照,所以這裡的 read 是快照讀 , 而非當前讀

快照讀: 就是 copy 一份尚未被 write 過的紀錄給你,所以並非真實的當前紀錄。
當前讀: 就是真實的那筆紀錄,而且只能讀取最新版本。

有了這個快照,就可以在就算該筆紀錄有 xlock 之下,讀取快照了,解決了 read-write conflict。

但,ReadView 是如何建構出來的呢?他是如何得知該取哪個版本的資料呢?
MySql 的表一樣有隱藏欄位,Mysql 會在每筆紀錄的最後加上 trx_id,
來紀錄最後一次"修改"該 row 的 txId, 所以 read 操作不會改到喔!
trx_id 其實就是 mysql 的 實作 mvcc 的 version
像這樣

id name (trx_id)
1 hello 999

而那些尚未 Commit 的 update,都會使用 link list 存在後面,類似 queue 的概念,代表著等待 commit 的修改。

這個 ReadView(snapshot) 就是我們發揮的地方了,
我們接下來介紹一下他主要判斷抓取版本的結構,這個結構不是在 ReadView 上,
是有點類似 global 的結構,讓每次要生成 ReadView 時做檢查該取哪個版本。
最後將對應的版本紀錄放到要建立的 ReadView(snapshot)上

欄位 意思
m_ids 當前還沒 commit/rollback 的 tx_ids
up_limit_id m_ids 的頭, 最小的 m_ids(最早的 tx)
low_limit_id m_ids 的尾, 最大的 m_ids(最晚的 tx)下一個 tx_id 的號碼牌
creator_trx_id 創建此 ReadView 的 txId

在 read commit level 下,當下 select 指令時, 會產生一個 ReadView(snapshot)的表

time sql 說明
1 begin
2 select create readview
3 ...
4 select create readview
n commit

在 Repeatable level 下,一個 tx 只會建立一個 ReadView(snapshot)的表

time sql 說明
1 begin
2 select create readview
3 ...
4 select 使用剛剛的 readview
n commit

ReadView 可見性

ReadView 會透過下面四個主要判斷句來決定該 record 是否該被當前 tx 看到
trx_id 是原表上的隱藏欄位,表示最後一個 write 該紀錄的 tx_id
以下範例為 read commit isolation level 下的規則

欄位 意思 是否可讀
trx_id=creator_trx_id 該 readview 是自己建立的
trx_id<up_limit_id 已經 commit 過的 tx
trx_id>low_limit_id 該 row 是此 readView 出生後才建立的 不可
up_limit_id<trx_id<low_limit_id 看 trx_id 是否在 m_ids 中 ,存在: 尚未 commit=>不可讀,不存在: 已經 commit=>可讀 存在:不可, 不存在:可

Read commit 下的 ReadView

我們就直接來舉的例子比較清楚

time tx1 tx2 tx3 trx_id readview
1 begin begin begin 0 m_ids=[1,2,3]
2 read(c=tx2) 0 m_ids=[1,2,3]
3 write 1 m_ids=[1,2,3]
4 read(c=tx2) 1 m_ids=[1,2,3]
5 commit write 3 m_ids=[2,3]
6 read(c=tx2) 3 m_ids=[2,3]
7 commit 3 m_ids=[2]
8 read(c=tx2) 3 m_ids=[2]
trx_id=creator_trx_id 的情況就是自己寫自己讀,這裡就暫時不做範例了
假設原資料 name=0
tx1 update name=tx1
tx3 update name=tx3

tx2 time2: 預期看到 name=0

//原表
{"name":0,"trx_id": 0}

//ReadView
{ "creator_trx_id": "tx2", "m_ids": [1, 2, 3], "up_limit_id": 1, "low_limit_id": 3}
// 取 {"name":0,"trx_id": 0}
  • 條件 1: creator_trx_id(tx2)=trx_id(0) false
  • 條件 2:
    • 原表 trx_id=0,[trx_id<=up_limit_id]<___<low_limit_id, return true,
  • 結論: readview 可以利用目前表上的資料,直接讀原表該紀錄的尾巴,即可建立 readview

tx2 time4: 預期看到 name=0

//原表
{"name":0,"trx_id": 0}<={"name":"tx1","trx_id": 1}

//ReadView
{ "creator_trx_id": "tx2", "m_ids": [1, 2, 3], "up_limit_id": 1, "low_limit_id": 3}
// 取 {"name":0,"trx_id": 0}
  • 條件 1: creator_trx_id(tx2)=trx_id(0) false
  • 條件 2:
    • 原表 trx_id=1,[up_limit_id<trx_id<low_limit_id], tx in m_ids return false, 往前找
    • 原表 trx_id=0,[trx_id<=up_limit_id]<__<low_limit_id, return true
  • 結論: readview 不可以讀取 tx1 上的修改,不可直接讀原表該紀錄的尾巴,往前找道符合規則的 txr_id=0 建立 readview

tx2 time6: 預期看到 name=tx1

//原表
//{"name":0,"trx_id": 0} override by tx1
{"name":"tx1","trx_id": 1}<={"name":"tx2","trx_id": 3}

//ReadView
{ "creator_trx_id": "tx2", "m_ids": [ 2, 3], "up_limit_id": 2, "low_limit_id": 3}
// 取 {"name":"tx1","trx_id": 1}
  • 條件 1: creator_trx_id(tx2)=trx_id(0) false

  • 條件 2:

    • 原表 trx_id=3, up_limit_id<___<[low_limit_id<=trx_id], return false, 往前找
    • 原表 trx_id=1, [trx_id<=up_limit_id]<___<=low_limit_id, return true
  • 結論: readveiw 不可以讀取 tx3 上的修改,不可直接讀原表該紀錄的尾巴,往前找道符合規則的 trx_id=1 建立 readview

tx2 time8: 預期看到 name=tx3

//原表
//{"name":0,"trx_id": 0} {"name":"tx1","trx_id": 1} override by tx1
{"name":"tx2","trx_id": 3}

//ReadView
{ "creator_trx_id": "tx2", "m_ids": [ 2 ], "up_limit_id": 2, "low_limit_id": 3}
// 取 {"name":"tx2","trx_id": 3}
  • 條件 1: creator_trx_id(tx2)=trx_id(0) false

  • 條件 2:

    • 原表 trx_id=3, [trx_id<=up_limit_id]<___<low_limit_id, return true
  • 結論: readview 可以利用目前表上的資料,直接讀原表該紀錄的尾巴,即可建立 readview

REPEATABLE READ 下的 ReadView

我們就直接來舉的例子比較清楚

time tx1 tx2 trx_id readview
1 begin begin 0 m_ids=[1,2]
2 read 0 m_ids=[1,2]
3 write 1 m_ids=[1,2]
4 commit 1 m_ids=[1,2]
5 read 1 m_ids=[2]
trx_id=creator_trx_id 的情況就是自己寫自己讀,這裡就暫時不做範例了
假設原資料 name=0
tx1 update name=tx1

REPEATABLE READ 只有 time2 會建立 readview, time5 直接使用 time2 的 readview

tx2 time2: 預期看到 name=0

//原表
{"name":0,"trx_id": 0}

//ReadView
{ "creator_trx_id": "tx2", "m_ids": [ 1,2 ], "up_limit_id": 1, "low_limit_id": 2}
// 取 {"name":0,"trx_id": 0}
  • 條件 1: creator_trx_id(tx2)=trx_id(0) false

  • 條件 2:

    • 原表 trx_id=3, [trx_id<=up_limit_id]<___<low_limit_id, return true
  • 結論: readview 可以利用目前表上的資料,直接讀原表該紀錄的尾巴,即可建立 readview, 記下這張 readview

tx2 time5: 預期看到 name=tx1

//原表
{"name":0,"trx_id": 0}<= {"name":"tx1","trx_id": 1}

// ReadView
// 直接抓上次的 {"name":0,"trx_id": 0}

  • 結論: 直接抓上次的 readview

結語

我們可以看到 mysql 透過 Readview 的方式建構出自己的 mvcc,
有了 readview 就可以在就算有 read-write conflict 時,做 read。
而 Readview 可以取哪個版本的資料就是透過 trx 與對該筆紀錄的類似 link list 的結構做版本控管。
其實該結構就是 mysql 所謂的 Undo Log,在後續的單元會做解釋。
知道了該取哪個版本的修改, 最後建立出相對應的 readview。
順利達到 mvcc 的效果。

參考資料

  1. MVCC 实现原理之 ReadView(一步到位)(上)
  2. MVCC 实现原理之 ReadView(一步到位)(下)

上一篇
[ACID] 樂觀鎖(MVCC In Postgres)
下一篇
[ACID] 樂觀鎖(MySQL RR Problem)
系列文
CRUD仔的一生(上集)32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言