摘要: 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一般会成为性能瓶颈点,预计这一特性会带来不错的吞吐量提高