摘要: MySQL8.0對json進行了比較完善的支持, 咱們知道json具備比較特殊的存儲格式,一般存在多個key value鍵值對,對於相似更新操做一般不會更新整個json列,而是某些鍵值。 對於某些複雜的應用,json列的數據可能會變的很是龐大,這時候一個突出的問題是:innodb並不識別json類型,對它而言這些存儲統一都是LOB類型,而在以前的版本中Innodb處理LOB更新的方式是標記刪除舊記錄,並插入新記錄,顯然這會帶來一些存儲上的開銷(儘管Purge線程會去後臺清理),而寫入的redo log和Binlog的量也會偏高,對於超大列,可能會嚴重影響到性能。mysql
MySQL8.0對json進行了比較完善的支持, 咱們知道json具備比較特殊的存儲格式,一般存在多個key value鍵值對,對於相似更新操做一般不會更新整個json列,而是某些鍵值。sql
對於某些複雜的應用,json列的數據可能會變的很是龐大,這時候一個突出的問題是:innodb並不識別json類型,對它而言這些存儲統一都是LOB類型,而在以前的版本中Innodb處理LOB更新的方式是標記刪除舊記錄,並插入新記錄,顯然這會帶來一些存儲上的開銷(儘管Purge線程會去後臺清理),而寫入的redo log和Binlog的量也會偏高,對於超大列,可能會嚴重影響到性能。爲了解決這個問題,MySQL8.0引入了LOB列部分更新的策略。json
官方博客有幾篇文章介紹的很是清楚,感興趣的能夠直接跳過本文,直接閱讀官方博客:mvc
1: partial update of json values
2: introduces lob index for faster update
3: MVCC of Large Objectsapp
以及相關的開發worklog:ide
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函數
本文僅僅是筆者在理解該特性時作的一些簡單的筆記,,記錄的主要目的是用於之後若是涉及到相關的工做能夠快速展開,所以比較凌亂sqlserver
目前partial update須要經過JSON_SET, 或者JSON_REPLACE等特定接口來進行json列的更新,而且不是全部的更新都可以知足條件:性能
空間足夠大,能夠容納替換的新值測試
下面以json_set更新json列爲例來看看相關的關鍵堆棧
如上所述,須要指定的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
), 存儲在bitmap結構TABLE::m_partial_update_columns
入口函數:TABLE::setup_partial_update()
在知足某些條件時,須要設置logical diff(用於記錄partial update列的binlog,下降binlog存儲開銷):
而後建立Partial_update_info對象(Table::m_partial_update_info
), 用於存儲partial update執行過程當中的狀態
當讀入一行記錄後,就須要根據sql語句來構建後鏡像,而對於partial update所涉及的json列,會作特殊處理:
Sql_cmd_update::update_single_table |--> fill_record_n_invoke_before_triggers |-->fill_record |--> Item::save_in_field |--> Item_func::save_possibly_as_json |--> Item_func_json_set_replace::val_json |--> Json_wrapper::attempt_binary_update |--> json_binary::Value::update_in_shadow |--> TABLE::add_binary_diff
json_wrapper::attempt_binary_update
: 作必要的數據類型檢查(是否符合partial update的條件)後,計算須要的空間,檢查是否有足夠的空閒空間Value::has_space()
來替換成新值。
Value::update_in_shadow
: 進一步將變化的數據存儲到binary diff對象中(TABLE::add_binary_diff
),每一個Binary_diff
對象包含了要修改對象的偏移量,長度以及一個指向新數據的const指針
以下例,摘自函數Value::update_in_shadow
的註釋,這裏提取出來,以便於理解json binary的格式,以及如何產生Binary Diff
建立測試表:
root@test 10:00:45>create table t (a int primary key, b json); Query OK, 0 rows affected (0.02 sec) root@test 10:01:06>insert into t values (1, '[ "abc", "def" ]'); Query OK, 1 row affected (0.07 sec)
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', 則空出一個字節出來:
root@test 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
,即經過json_remove從列上移除一個字段,如下樣例一樣摘自函數的註釋:
json列的值爲
{ "a": "x", "b": "y", "c": "z" } 存儲格式: 0x00 - type: JSONB_TYPE_SMALL_OBJECT 0x03 - 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) 0x1a - offset of key "b" (high byte) 0x00 - offset of key "b" (low byte) 0x01 - length of key "b" (high byte) 0x00 - length of key "b" (low byte) 0x1b - offset of key "c" (high byte) 0x00 - offset of key "c" (low byte) 0x01 - length of key "c" (high byte) 0x00 - length of key "c" (low byte) 0x0c - type of value "a": JSONB_TYPE_STRING 0x1c - offset of value "a" (high byte) 0x00 - offset of value "a" (low byte) 0x0c - type of value "b": JSONB_TYPE_STRING 0x1e - offset of value "b" (high byte) 0x00 - offset of value "b" (low byte) 0x0c - type of value "c": JSONB_TYPE_STRING 0x20 - offset of value "c" (high byte) 0x00 - offset of value "c" (low byte) 0x61 - first key ('a') 0x62 - second key ('b') 0x63 - third key ('c') 0x01 - length of value "a" 0x78 - contents of value "a" ('x') 0x01 - length of value "b" 0x79 - contents of value "b" ('y') 0x01 - length of value "c" 0x7a - contents of value "c" ('z')
將其中的成員$.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
中,
logical diff 主要用於優化寫binlog
Sql_cmd_update::update_single_table |--> fill_record_n_invoke_before_triggers |-->fill_record |--> Item::save_in_field |--> Item_func::save_possibly_as_json |--> Item_func_json_set_replace::val_json |-->TABLE::add_logical_diff
相關代碼:
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_ID | 4 | space id |
BTR_EXTERN_PAGE_NO | 4 | 第一個 lob page的no |
BTR_EXTERN_OFFSET/BTR_EXTERN_VERSION | 4 | 新的格式記錄version號 |
第一個FIL_PAGE_TYPE_LOG_FIRST頁面的操做定義在 lob::first_page_t類中格式以下(參考文件: include/lob0first.h lob/lob0first.cc):
字段 | 字節數 | 描述 |
---|---|---|
OFFSET_VERSION | 1 | 表示lob的版本號,當前爲0,用於之後lob格式改變作版本區分 |
OFFSET_FLAGS | 1 | 目前只使用第一個bit,被設置時表示沒法作partial update, 用於通知purge線程某個更新操做產生的老版本LOB能夠被徹底釋放掉 |
OFFSET_LOB_VERSION | 4 | 每一個lob page都有個版本號,初始爲1,每次更新後遞增 |
OFFSET_LAST_TRX_ID | 6 | |
OFFSET_LAST_UNDO_NO | 4 | |
OFFSET_DATA_LEN | 4 | 存儲在該page上的數據長度 |
OFFSET_TRX_ID | 6 | 建立存儲在該page上的事務id |
OFFSET_INDEX_LIST | 16 | 維護lob page鏈表 |
OFFSET_INDEX_FREE_NODES | 16 | 維護空閒節點 |
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_VERSION | 1 | lob data version,當前爲0 |
OFFSET_DATA_LEN | 4 | 數據長度 |
OFFSET_TRX_ID | 6 | 建立該lob page的事務Id |
LOB_PAGE_DATA | lob data開始的位置 |
lob index entry的大小爲60字節,主要包含以下內容(include/lob0index.h lob/lob0index.cc):
偏移量 | 字節數 | 描述 |
---|---|---|
OFFSET_PREV | 6 | Pointer to the previous index entry |
OFFSET_NEXT | 6 | Pointer to the next index entry |
OFFSET_VERSIONS | 16 | Pointer to the list of old versions for this index entry |
OFFSET_TRXID | 6 | The creator transaction identifier. |
OFFSET_TRXID_MODIFIER | 6 | The modifier transaction identifier |
OFFSET_TRX_UNDO_NO | 4 | the undo number of creator transaction. |
OFFSET_TRX_UNDO_NO_MODIFIER | 4 | The undo number of modifier transaction. |
OFFSET_PAGE_NO | 4 | The page number of LOB data page |
OFFSET_DATA_LEN | 4 | The amount of LOB data it contains in bytes. |
OFFSET_LOB_VERSION | 4 | The LOB version number to which this index entry belongs. |
從index entry的記錄格式咱們能夠看到 兩個關鍵信息:
EXTERN REF (v2) | LOB IDX ENTRY (v1) | LOB IDX ENTRY(v2) -----> LOB IDX ENTRY(v1) | LOG IDX ...(v1)
多版本讀判斷參考函數 'lob::read'
lob更新lob::update
: 根據binary diff,依次replace
Note: 不是全部的lob數據都須要partial update, 額外的lob index一樣會帶來存儲開銷,所以定義了一個threshold(ref_t::LOB_BIG_THRESHOLD_SIZE),超過2個page纔去作partial update; 另外row_format也要確保lob列不存儲列前綴到clust index ( ref btr_store_big_rec_extern_fields
)
在更新完一行後,對應的變動須要打包到線程的cache中(THD::binlog_write_row() --> pack_row()
), 這時候要對partial update進行特殊處理,須要設置特定選項:
如上例第一個update產生的binlog以下:
UPDATE t SET b = JSON_SET(b, '$[0]', 'XY'); binlog: '/*!*/; ### UPDATE `test`.`t` ### WHERE ### @1=1 /* INT meta=0 nullable=0 is_null=0 */ ### SET ### @2=JSON_REPLACE(@2, '$[0]', 'XY') /* JSON meta=4 nullable=1 is_null=0 */
因爲存在主鍵,所以前鏡像只記錄了主鍵值,然後鏡像也只記錄了須要更新的列的內容,對於超大Json列,binlog上的開銷也是極小的,考慮到binlog一般會成爲性能瓶頸點,預計這一特性會帶來不錯的吞吐量提高