MySQL8.0 新特性:Partial Update of LOB Column-阿里云开发者社区

开发者社区> 阿里云数据库> 正文
登录阅读全文

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

简介: MySQL8.0对json进行了比较完善的支持, 我们知道json具有比较特殊的存储格式,通常存在多个key value键值对,对于类似更新操作通常不会更新整个json列,而是某些键值。 对于某些复杂的应用,json列的数据可能会变的非常庞大,这时候一个突出的问题是:innodb并不识别json类型,对它而言这些存储统一都是LOB类型,而在之前的版本中Innodb处理LOB更新的方式是标记删除旧记录,并插入新记录,显然这会带来一些存储上的开销(尽管Purge线程会去后台清理),而写入的redo log和Binlog的量也会偏高,对于超大列,可能会严重影响到性能。

Update history:

  1. Update: 2018/7/28 MySQL8.0.12
    https://dev.mysql.com/worklog/task/?id=11328

InnoDB: Undo logging is now supported for small updates to large object (LOB) data, which improves performance of LOB updates that are 100 bytes in size or less. Previously, LOB updates were a minimum of one LOB page in size, which is less than optimal for updates that might only modify a few bytes. This enhancement builds upon support added in MySQL 8.0.4 for partial fetch and update of LOB data.

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
4: MySQL 8.0: Optimizing Small Partial Update of LOB in InnoDB

以及相关的开发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), 存储在bitmap结构TABLE::m_partial_update_columns

设置partial update

入口函数:TABLE::setup_partial_update()

在满足某些条件时,需要设置logical diff(用于记录partial update列的binlog,降低binlog存储开销):

  • binlog_row_value_options设置为"partial_json"
  • binlog 打开
  • log_bin_use_v1_row_events关闭
  • 使用row format

然后创建Partial_update_info对象(Table::m_partial_update_info), 用于存储partial update执行过程中的状态

  • Table::m_enabled_logical_diff_columns
  • TABLE::m_binary_diff_vectors
  • TABLE::m_logical_diff_vectors

创建更新向量

当读入一行记录后,就需要根据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

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
                                    

新的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_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的记录格式我们可以看到 两个关键信息:

  • 对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: 根据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)

写入binlog

在更新完一行后,对应的变更需要打包到线程的cache中(THD::binlog_write_row() --> pack_row()), 这时候要对partial update进行特殊处理,需要设置特定选项:

  • binlog_row_image = MINIMAL;
  • binlog_row_value_options=PARTIAL_JSON

如上例第一个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通常会成为性能瓶颈点,预计这一特性会带来不错的吞吐量提升

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
阿里云数据库
使用钉钉扫一扫加入圈子
+ 订阅

帮用户承担一切数据库风险,给您何止是安心!

官方博客
链接