MySQL8.0 新特性:Partial Update of LOB Column

NO IMAGE
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

摘要: MySQL8.0對json進行了比較完善的支援, 我們知道json具有比較特殊的儲存格式,通常存在多個key value鍵值對,對於類似更新操作通常不會更新整個json列,而是某些鍵值。 對於某些複雜的應用,json列的資料可能會變的非常龐大,這時候一個突出的問題是:innodb並不識別json型別,對它而言這些儲存統一都是LOB型別,而在之前的版本中Innodb處理LOB更新的方式是標記刪除舊記錄,並插入新記錄,顯然這會帶來一些儲存上的開銷(儘管Purge執行緒會去後臺清理),而寫入的redo log和Binlog的量也會偏高,對於超大列,可能會嚴重影響到效能。

MySQL8.0對json進行了比較完善的支援, 我們知道json具有比較特殊的儲存格式,通常存在多個key value鍵值對,對於類似更新操作通常不會更新整個json列,而是某些鍵值。

對於某些複雜的應用,json列的資料可能會變的非常龐大,這時候一個突出的問題是:innodb並不識別json型別,對它而言這些儲存統一都是LOB型別,而在之前的版本中Innodb處理LOB更新的方式是標記刪除舊記錄,並插入新記錄,顯然這會帶來一些儲存上的開銷(儘管Purge執行緒會去後臺清理),而寫入的redo log和Binlog的量也會偏高,對於超大列,可能會嚴重影響到效能。為了解決這個問題,MySQL8.0引入了LOB列部分更新的策略。

官方部落格有幾篇文章介紹的非常清楚,感興趣的可以直接跳過本文,直接閱讀官方部落格:

1: partial update of json values
2: introduces lob index for faster update
3: MVCC of Large Objects

以及相關的開發worklog:

WL#8963: Support for partial update of JSON in the optimizer
WL#8985: InnoDB: Refactor compressed BLOB code to facilitate partial fetch/update
WL#9141: InnoDB: Refactor uncompressed BLOB code to facilitate partial fetch/update
WL#9263: InnoDB: Enable partial access of LOB using multiple zlib streams
WL#8960: InnoDB: Partial Fetch and Update of BLOB
WL#10570: Provide logical diffs for partial update of JSON values
WL#2955: RBR replication of partial JSON updates

本文僅僅是筆者在理解該特性時做的一些簡單的筆記,,記錄的主要目的是用於以後如果涉及到相關的工作可以快速展開,因此比較凌亂

目前partial update需要通過JSON_SET, 或者JSON_REPLACE等特定介面來進行json列的更新,並且不是所有的更新都能夠滿足條件:

沒有增加新的元素

空間足夠大,可以容納替換的新值

但類似資料長度(10 =>更新成7=>更新成9)是允許的

下面以json_set更新json列為例來看看相關的關鍵堆疊

檢查是否支援partial update

如上所述,需要指定的json函式介面才能進行partial update

mysql_execute_command
|--> Sql_cmd_dml::execute
|--> Sql_cmd_dml::prepare
|--> Sql_cmd_update::prepare_inner
|---> prepare_partial_update
|-->Item_json_func::supports_partial_update

這裡只是做預檢查,對於json列的更新如果全部是通過json_set/replace/remove進行的,則將其標記為候選partial update的列(TABLE::mark_column_for_partial_update

json_wrapper::attempt_binary_update

json資料的儲存格式如下:

        0x02 - type: small JSON array
0x02 - number of elements (low byte)
0x00 - number of elements (high byte)
0x12 - number of bytes (low byte)
0x00 - number of bytes (high byte)
0x0C - type of element 0 (string)
0x0A - offset of element 0 (low byte)
0x00 - offset of element 0 (high byte)
0x0C - type of element 1 (string)
0x0E - offset of element 1 (low byte)
0x00 - offset of element 1 (high byte)
0x03 - length of element 0
'a'
'b'  - content of element 0
'c'
0x03 - length of element 1
'd'
'e'  - content of element 1
'f'

更新json列的’abc’為’XY’, 則空出一個位元組出來:

[email protected] 10:01:39>UPDATE t SET b = JSON_SET(b, '$[0]', 'XY');
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

此時的儲存格式為:

              0x02 - type: small JSON array
0x02 - number of elements (low byte)
0x00 - number of elements (high byte)
0x12 - number of bytes (low byte)
0x00 - number of bytes (high byte)
0x0C - type of element 0 (string)
0x0A - offset of element 0 (low byte)
0x00 - offset of element 0 (high byte)
0x0C - type of element 1 (string)
0x0E - offset of element 1 (low byte)
0x00 - offset of element 1 (high byte)
CHANGED       0x02 - length of element 0
CHANGED           'X'
CHANGED           'Y'  - content of element 0
(free)  'c'
0x03 - length of element 1
'd'
'e'  - content of element 1
'f'

此處隻影響到一個element,因此 只有一個binary diff

再執行更新:

UPDATE t SET j = JSON_SET(j, '$[1]', 'XYZW')

第二個element從3個位元組更新成4個位元組,顯然原地沒有足夠的空間,但可以利用其一個element的剩餘空間

              0x02 - type: small JSON array
0x02 - number of elements (low byte)
0x00 - number of elements (high byte)
0x12 - number of bytes (low byte)
0x00 - number of bytes (high byte)
0x0C - type of element 0 (string)
0x0A - offset of element 0 (low byte)
0x00 - offset of element 0 (high byte)
0x0C - type of element 1 (string)
CHANGED 0x0D - offset of element 1 (low byte)
0x00 - offset of element 1 (high byte)
0x02 - length of element 0
'X'  - content of element 0
'Y'  - content of element 0
CHANGED         0x04 - length of element 1
CHANGED         'X'
CHANGED         'Y'
CHANGED         'Z'  - content of element 1
CHANGED         'W'

這裡會產生兩個binary diff,一個更新offset, 一個更新資料

我們再執行一條update,將字串修改成整數,這種情況下,原來儲存字串offset的位置被更改成了整數,而原來字串佔用的空間變成Unused狀態。這裡只

UPDATE t SET b= JSON_SET(b, '$[1]', 456)

        0x02 - type: small JSON array
0x02 - number of elements (low byte)
0x00 - number of elements (high byte)
0x12 - number of bytes (low byte)
0x00 - number of bytes (high byte)
0x0C - type of element 0 (string)
0x0A - offset of element 0 (low byte)
0x00 - offset of element 0 (high byte)
CHANGED 0x05 - type of element 1 (int16)
CHANGED 0xC8 - value of element 1 (low byte)
CHANGED 0x01 - value of element 1 (high byte)
0x02 - length of element 0
'X'  - content of element 0
'Y'  - content of element 0
(free)  0x04 - length of element 1
(free)  'X' 
(free)  'Y'
(free)  'Z'  - content of element 1
(free)  'W

型別從string變成int16,使用之前offset的欄位記錄int值,而原來string的空間則變成空閒狀態, 這裡產生一個binary diff。

我們再來看看另外一個相似的函式Value::remove_in_shadow

將其中的成員$.b移除掉:

UPDATE t SET j = JSON_REMOVE(j, '$.b');
格式為:
0x00 - type: JSONB_TYPE_SMALL_OBJECT
CHANGED 0x02 - number of elements (low byte)
0x00 - number of elements (high byte)
0x22 - number of bytes (low byte)
0x00 - number of bytes (high byte)
0x19 - offset of key "a" (high byte)
0x00 - offset of key "a" (low byte)
0x01 - length of key "a" (high byte)
0x00 - length of key "a" (low byte)
CHANGED 0x1b - offset of key "c" (high byte)
CHANGED 0x00 - offset of key "c" (low byte)
CHANGED 0x01 - length of key "c" (high byte)
CHANGED 0x00 - length of key "c" (low byte)
CHANGED 0x0c - type of value "a": JSONB_TYPE_STRING
CHANGED 0x1c - offset of value "a" (high byte)
CHANGED 0x00 - offset of value "a" (low byte)
CHANGED 0x0c - type of value "c": JSONB_TYPE_STRING
CHANGED 0x20 - offset of value "c" (high byte)
CHANGED 0x00 - offset of value "c" (low byte)
(free)  0x00
(free)  0x0c
(free)  0x1e
(free)  0x00
(free)  0x0c
(free)  0x20
(free)  0x00
0x61 - first key  ('a')
(free)  0x62
0x63 - third key  ('c')
0x01 - length of value "a"
0x78 - contents of value "a" ('x')
(free)  0x01
(free)  0x79
0x01 - length of value "c"
0x7a - contents of value "c" ('z')

這裡會產生兩個binary diff,一個用於更新element個數,一個用於更新offset。

從上面的例子可以看到,每個Binary diff表示了一段連續更新的資料,有幾段連續更新的資料,就有幾個binary diff。 binary diff儲存到TABLE::m_partial_update_info->m_binary_diff_vectors

新的LOB儲存格式

相關程式碼:
storage/innobase/lob/*, 所有的類和函式定義在namesapce lob下面

從上面的分析可以看到,Server層已經提供了所有修改的偏移量,新資料長度,已經判斷好了資料能夠原地儲存,對於innodb,則須要利用這些資訊來實現partial update 。

在展開這個問題之前,我們先來看下innodb針對json列的新格式。從程式碼中可以看到,為了實現partial update, innodb增加了幾種新的資料頁格式:

壓縮表:
FIL_PAGE_TYPE_ZLOB_FIRST
FIL_PAGE_TYPE_ZLOB_DATA
FIL_PAGE_TYPE_ZLOB_INDEX
FIL_PAGE_TYPE_ZLOB_FRAG
FIL_PAGE_TYPE_ZLOB_FRAG_ENTRY
普通表:
FIL_PAGE_TYPE_LOB_INDEX
FIL_PAGE_TYPE_LOB_DATA
FIL_PAGE_TYPE_LOB_FIRST

我們知道,傳統的LOB列通常是在聚集索引記錄內留一個外部儲存指標,指向lob儲存的page,如果一個page儲存不下,就會產生lob page連結串列。而新的儲存格式,則引入了lob index的概念,也就是為所有的lob page建立索引,格式如下:

ref pointer in cluster record
-------
|
FIL_PAGE_TYPE_LOG_FIRST
|
FIL_PAGE_TYPE_LOB_INDEX             ----------->   FIL_PAGE_TYPE_LOB_DATA
|
FIL_PAGE_TYPE_LOB_INDEX             -------------> FIL_PAGE_TYPE_LOB_DATA
|
... ....

Note: 本文只討論非壓縮表的場景, 對於壓縮表引入了更加複雜的資料型別,以後有空再在本文補上。

ref Pointer格式如下(和之前相比,增加了版本號)

欄位位元組數描述
BTR_EXTERN_SPACE_ID4space id
BTR_EXTERN_PAGE_NO4第一個 lob page的no
BTR_EXTERN_OFFSET/BTR_EXTERN_VERSION4新的格式記錄version號

第一個FIL_PAGE_TYPE_LOG_FIRST頁面的操作定義在 lob::first_page_t類中格式如下(參考檔案: include/lob0first.h lob/lob0first.cc):

欄位位元組數描述
OFFSET_VERSION1表示lob的版本號,當前為0,用於以後lob格式改變做版本區分
OFFSET_FLAGS1目前只使用第一個bit,被設定時表示無法做partial update, 用於通知purge執行緒某個更新操作產生的老版本LOB可以被完全釋放掉
OFFSET_LOB_VERSION4每個lob page都有個版本號,初始為1,每次更新後遞增
OFFSET_LAST_TRX_ID6
OFFSET_LAST_UNDO_NO4
OFFSET_DATA_LEN4儲存在該page上的資料長度
OFFSET_TRX_ID6建立儲存在該page上的事務id
OFFSET_INDEX_LIST16維護lob page連結串列
OFFSET_INDEX_FREE_NODES16維護空閒節點
LOB_PAGE_DATA 儲存資料的起始位置,注意第一個page同時包含了lob index 和lob data,但在第一個lob page中只包含了10個lob index記錄,每個lob index大小為60位元組

除了第一個lob page外,其他所有的lob page都是通過lob index記錄來指向的,lob index之間連結成連結串列,每個index entry指向一個lob page,

普通Lob Page的格式如下

欄位位元組數描述
OFFSET_VERSION1lob data version,當前為0
OFFSET_DATA_LEN4資料長度
OFFSET_TRX_ID6建立該lob page的事務Id
LOB_PAGE_DATA lob data開始的位置

lob index entry的大小為60位元組,主要包含如下內容(include/lob0index.h lob/lob0index.cc):

偏移量位元組數描述
OFFSET_PREV6Pointer to the previous index entry
OFFSET_NEXT6Pointer to the next index entry
OFFSET_VERSIONS16Pointer to the list of old versions for this index entry
OFFSET_TRXID6The creator transaction identifier.
OFFSET_TRXID_MODIFIER6The modifier transaction identifier
OFFSET_TRX_UNDO_NO4the undo number of creator transaction.
OFFSET_TRX_UNDO_NO_MODIFIER4The undo number of modifier transaction.
OFFSET_PAGE_NO4The page number of LOB data page
OFFSET_DATA_LEN4The amount of LOB data it contains in bytes.
OFFSET_LOB_VERSION4The LOB version number to which this index entry belongs.

從index entry的記錄格式我們可以看到 兩個關鍵資訊:

對lob page的修改會產生新的lob page(“lob::replace()”) 和新的lob index entry
lob page no及其資料長度,據此我們可以根據修改的資料在json column裡的offset,通過lob index快速的定位到其所在的lob page
每個lob page的版本號: 為了實現mvcc多版本,使用者執行緒先從undo log中找到對應版本的clust record,找出其中儲存的版本號v1,然後在掃描lob index時,如index entry中記錄的版本號<= v1,則是可見的,如果> v1, 那麼就需要根據OFFSET_VERSIONS連結串列,找到對應版本的index entry,並
根據這個老的Index entry找到對應的lob page, 如下所示:

EXTERN REF (v2)
|
LOB IDX ENTRY (v1)
|
LOB IDX ENTRY(v2)  -----> LOB IDX ENTRY(v1)
|
LOG IDX ...(v1)

多版本讀判斷參考函式 ‘lob::read’
lob更新lob::update

由於存在主鍵,因此前映象只記錄了主鍵值,而後映象也只記錄了需要更新的列的內容,對於超大Json列,binlog上的開銷也是極小的,考慮到binlog通常會成為效能瓶頸點,預計這一特性會帶來不錯的吞吐量提升

原文連結

本文為雲棲社群原創內容,未經允許不得轉載。


(adsbygoogle = window.adsbygoogle || []).push({});

function googleAdJSAtOnload() {
var element = document.createElement(“script”);
element.src = “//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js”;
element.async = true;
document.body.appendChild(element);
}
if (window.addEventListener) {
window.addEventListener(“load”, googleAdJSAtOnload, false);
} else if (window.attachEvent) {
window.attachEvent(“onload”, googleAdJSAtOnload);
} else {
window.onload = googleAdJSAtOnload;
}

資料庫 最新文章