前面介紹了樂觀鎖的原理,這裡將介紹在 Mysql 中,是如何利用樂觀鎖的原理來建構 MVCC DB 的。
在理解 mysql mvcc 時,這裡會有些不一樣的地方
mvcc 的重點實作在於當某筆資料要被 read 時,
隨時可以被 Read, 不會因為某 Tx 在 write 後尚未 commit 而卡住不可讀。
因為 mysql 的 isolation level 是透過 sxlock 實作的,
所以他的 read/write 是真的會對紀錄上鎖的。
又因為是無法直接採用增加類似 version 欄位來實作樂觀鎖就算加上也會因 lock 住而無法讀取,
所以不能像是 postgres 透過一些隱藏欄位+clog 的方式來實作 isolation level 。
我們知道 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 的概念
意思就是在 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 會透過下面四個主要判斷句來決定該 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=>可讀 | 存在:不可, 不存在:可 |
我們就直接來舉的例子比較清楚
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] |
假設原資料 name=0
tx1 update name=tx1
tx3 update name=tx3
//原表
{"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}
[trx_id<=up_limit_id]<___<low_limit_id
, return true,//原表
{"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}
[up_limit_id<trx_id<low_limit_id]
, tx in m_ids return false, 往前找[trx_id<=up_limit_id]<__<low_limit_id
, return true//原表
//{"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:
up_limit_id<___<[low_limit_id<=trx_id]
, return false, 往前找[trx_id<=up_limit_id]<___<=low_limit_id
, return true結論: readveiw 不可以讀取 tx3 上的修改,不可直接讀原表該紀錄的尾巴,往前找道符合規則的 trx_id=1 建立 readview
//原表
//{"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<=up_limit_id]<___<low_limit_id
, return true結論: readview 可以利用目前表上的資料,直接讀原表該紀錄的尾巴,即可建立 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] |
假設原資料 name=0
tx1 update name=tx1
REPEATABLE READ 只有 time2 會建立 readview, time5 直接使用 time2 的 readview
//原表
{"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<=up_limit_id]<___<low_limit_id
, return true結論: readview 可以利用目前表上的資料,直接讀原表該紀錄的尾巴,即可建立 readview, 記下這張 readview
//原表
{"name":0,"trx_id": 0}<= {"name":"tx1","trx_id": 1}
// ReadView
// 直接抓上次的 {"name":0,"trx_id": 0}
我們可以看到 mysql 透過 Readview 的方式建構出自己的 mvcc,
有了 readview 就可以在就算有 read-write conflict 時,做 read。
而 Readview 可以取哪個版本的資料就是透過 trx 與對該筆紀錄的類似 link list 的結構做版本控管。
其實該結構就是 mysql 所謂的 Undo Log,在後續的單元會做解釋。
知道了該取哪個版本的修改, 最後建立出相對應的 readview。
順利達到 mvcc 的效果。