行业网站排名,WordPress住小程序,最近发生的重大新闻事件,营销是做什么摘要#xff1a; MySQL8.0对json进行了比较完善的支持, 我们知道json具有比较特殊的存储格式#xff0c;通常存在多个key value键值对#xff0c;对于类似更新操作通常不会更新整个json列#xff0c;而是某些键值。 对于某些复杂的应用#xff0c;json列的数据可能会变的非…摘要 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列部分更新的策略。本文仅仅是笔者在理解该特性时做的一些简单的笔记记录的主要目的是用于以后如果涉及到相关的工作可以快速展开因此比较凌乱目前partial update需要通过JSON_SET, 或者JSON_REPLACE等特定接口来进行json列的更新并且不是所有的更新都能够满足条件没有增加新的元素空间足够大可以容纳替换的新值但类似数据长度(10 更新成7更新成9)是允许的下面以json_set更新json列为例来看看相关的关键堆栈检查是否支持partial update如上所述需要指定的json函数接口才能进行partial updatemysql_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_jsonbinlog 打开log_bin_use_v1_row_events关闭使用row format然后创建Partial_update_info对象(Table::m_partial_update_info) 用于存储partial update执行过程中的状态Table::m_enabled_logical_diff_columnsTABLE::m_binary_diff_vectorsTABLE::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_diffjson_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创建测试表roottest 10:00:45create table t (a int primary key, b json);Query OK, 0 rows affected (0.02 sec)roottest 10:01:06insert into t values (1, [ abc, def ]);Query OK, 1 row affected (0.07 sec)json数据的存储格式如下0x02 - type: small JSON array0x02 - 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 0ab - content of element 0c0x03 - length of element 1de - content of element 1f更新json列的abc为XY, 则空出一个字节出来:roottest 10:01:39UPDATE 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 array0x02 - 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 0CHANGED XCHANGED Y - content of element 0(free) c0x03 - length of element 1de - content of element 1f此处只影响到一个element因此 只有一个binary diff再执行更新UPDATE t SET j JSON_SET(j, $[1], XYZW)第二个element从3个字节更新成4个字节显然原地没有足够的空间但可以利用其一个element的剩余空间0x02 - type: small JSON array0x02 - 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 0X - content of element 0Y - content of element 0CHANGED 0x04 - length of element 1CHANGED XCHANGED YCHANGED Z - content of element 1CHANGED W这里会产生两个binary diff一个更新offset 一个更新数据我们再执行一条update将字符串修改成整数这种情况下原来存储字符串offset的位置被更改成了整数而原来字符串占用的空间变成Unused状态。这里只UPDATE t SET b JSON_SET(b, $[1], 456)0x02 - type: small JSON array0x02 - 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 0X - content of element 0Y - 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_OBJECT0x03 - 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_STRING0x1c - offset of value a (high byte)0x00 - offset of value a (low byte)0x0c - type of value b: JSONB_TYPE_STRING0x1e - offset of value b (high byte)0x00 - offset of value b (low byte)0x0c - type of value c: JSONB_TYPE_STRING0x20 - 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 a0x78 - contents of value a (x)0x01 - length of value b0x79 - contents of value b (y)0x01 - length of value c0x7a - contents of value c (z)将其中的成员$.b移除掉UPDATE t SET j JSON_REMOVE(j, $.b);格式为0x00 - type: JSONB_TYPE_SMALL_OBJECTCHANGED 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_STRINGCHANGED 0x1c - offset of value a (high byte)CHANGED 0x00 - offset of value a (low byte)CHANGED 0x0c - type of value c: JSONB_TYPE_STRINGCHANGED 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) 0x000x61 - first key (a)(free) 0x620x63 - third key (c)0x01 - length of value a0x78 - contents of value a (x)(free) 0x01(free) 0x790x01 - length of value c0x7a - 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 difflogical diff 主要用于优化写binlogSql_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_FIRSTFIL_PAGE_TYPE_ZLOB_DATAFIL_PAGE_TYPE_ZLOB_INDEXFIL_PAGE_TYPE_ZLOB_FRAGFIL_PAGE_TYPE_ZLOB_FRAG_ENTRY普通表FIL_PAGE_TYPE_LOB_INDEXFIL_PAGE_TYPE_LOB_DATAFIL_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 idBTR_EXTERN_PAGE_NO 4 第一个 lob page的noBTR_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 6OFFSET_LAST_UNDO_NO 4OFFSET_DATA_LEN 4 存储在该page上的数据长度OFFSET_TRX_ID 6 创建存储在该page上的事务idOFFSET_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当前为0OFFSET_DATA_LEN 4 数据长度OFFSET_TRX_ID 6 创建该lob page的事务IdLOB_PAGE_DATA lob data开始的位置lob index entry的大小为60字节主要包含如下内容(include/lob0index.h lob/lob0index.cc):偏移量 字节数 描述OFFSET_PREV 6 Pointer to the previous index entryOFFSET_NEXT 6 Pointer to the next index entryOFFSET_VERSIONS 16 Pointer to the list of old versions for this index entryOFFSET_TRXID 6 The creator transaction identifier.OFFSET_TRXID_MODIFIER 6 The modifier transaction identifierOFFSET_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 pageOFFSET_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 entrylob 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::readlob更新lob::update: 根据binary diff依次replaceNote 不是所有的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_optionsPARTIAL_JSON如上例第一个update产生的binlog如下UPDATE t SET b JSON_SET(b, $[0], XY);binlog:/*!*/;### UPDATE test.t### WHERE### 11 /* INT meta0 nullable0 is_null0 */### SET### 2JSON_REPLACE(2, $[0], XY) /* JSON meta4 nullable1 is_null0 */由于存在主键因此前镜像只记录了主键值而后镜像也只记录了需要更新的列的内容对于超大Json列binlog上的开销也是极小的考虑到binlog通常会成为性能瓶颈点预计这一特性会带来不错的吞吐量提升原文链接