赞
踩
本篇文章以 MySQL 5.7 版本为例。
官方文档:https://dev.mysql.com/doc/refman/5.7/en/json.html
5.7.8 及以后的版本才支持,遵循 RFC7159 标准(https://datatracker.ietf.org/doc/html/rfc7159)。
json 内容只能是 json object 或者 json array。
支持的 json 元素的数据类型:
类型 | 对应的十六进制编码 |
---|---|
SMALL_OBJECT | 0x0 |
LARGE_OBJECT | 0x1 |
SMALL_ARRAY | 0x2 |
LARGE_ARRAY | 0x3 |
LITERAL | 0x4 |
INT16 | 0x5 |
UINT16 | 0x6 |
INT32 | 0x7 |
UINT32 | 0x8 |
INT64 | 0x9 |
UINT64 | 0xA |
DOUBLE | 0xB |
STRING | 0xC |
OPAQUE | 0xF |
其中 LITERAL 类型比较特殊,对应以下 3 种值:
NULL | \x00 |
TRUE | \x01 |
FALSE | \x02 |
关于 json 数据的序列化、反序列化,主要逻辑都在 mysql-5.7.44\sql\json_binary.cc 文件中,另外 3 个文件也可以了解一下。
mysql-5.7.44\sql\json_binary.h
mysql-5.7.44\sql\json_dom.h
mysql-5.7.44\sql\json_dom.cc
json 的存储由这几部分构成:
类型 | 占1个字节 |
元素个数 | 小类型占2个字节 大类型占4个字节 |
占用字节数 | 小类型占2个字节 大类型占4个字节 |
key_entrys | json object 专有 由 offset + size 组成 offset (表示每个 key 所在的脚标位置), size (表示每个 key 本身占的字节数) 小类型占2个字节 大类型占4个字节 |
value_entrys | 由type + inlined_value/offset 组成 type 占1个字节和首字节类似 J_NULL、J_BOOLEAN、J_INT、J_UINT 这几种数据类型属于 inlined_value,存储的是真实的值;其他的数据类型则存储每个 value 所在的脚标位置 小类型占2个字节 大类型占4个字节 |
keys | json object 专有 依次存储每个key的内容 |
values | 依次存储每个value的内容 由 size + value 组成 size 表示每个value占用的字节数,size最多占1~4个字节 value 表示真实的value 内容 |
也就是说,如果存储的是 json object,就是上面的结构。
大小类型的判断依据是下面这段逻辑:
static bool is_too_big_for_json(size_t offset_or_size, bool large)
{
if (offset_or_size > UINT_MAX16)
{
if (!large)
return true;
if (offset_or_size > UINT_MAX32)
{
my_error(ER_JSON_VALUE_TOO_BIG, MYF(0));
return true;
}
}
概括来说就是,数组元素个数 / 对象key的个数 / 脚标位置 / 内容本身的size 小于 65535(2^16 - 1) 都是小类型, 65535 < < 4294967295 (2^32 - 1) 的都是大类型,再大的就不支持了。
这就是存储组成中,小类型的用 2 个字节,大类型的用 4 个字节的原因。
如果是 json array,结构如下:
类型 | |
元素个数 | |
占用字节数 | |
value_entrys | |
values |
先建一张表
CREATE TABLE facts (sentence JSON);
3.1 存储一个 json array
插入一条 json arrsy 数据
INSERT INTO facts VALUES('[42, "xy", true]');
接下来就跟随代码逻辑,一步一步看看插入的 json 内容是如何序列化到磁盘上的。
从 serialize 这个方法作为切入口(下文如无特殊说明,都是在 json_binary.cc 文件中)
- bool serialize(const Json_dom *dom, String *dest)
- {
- // Reset the destination buffer.
- dest->length(0);
- dest->set_charset(&my_charset_bin);
-
-
- // Reserve space (one byte) for the type identifier.
- if (dest->append('\0'))
- return true; /* purecov: inspected */
- return serialize_json_value(dom, 0, dest, 0, false) != OK;
- }
这个方法主要是把待序列化的 *dom 序列化后,存储在 String 类型的 *dest 的中。
没啥可说的,主要逻辑在z下面这个方法 serialize_json_value 中
- static enum_serialization_result
- serialize_json_value(const Json_dom *dom, size_t type_pos, String *dest,
- size_t depth, bool small_parent)
- {
- const size_t start_pos= dest->length();
- assert(type_pos < start_pos);
-
-
- enum_serialization_result result;
-
-
- switch (dom->json_type())
- {
- case Json_dom::J_ARRAY:
- {
- const Json_array *array= down_cast<const Json_array*>(dom);
- (*dest)[type_pos]= JSONB_TYPE_SMALL_ARRAY;
- result= serialize_json_array(array, dest, false, depth);
-
-
- if (result == VALUE_TOO_BIG)
- {
- // If the parent uses the small storage format, it needs to grow too.
- if (small_parent)
- return VALUE_TOO_BIG;
- dest->length(start_pos);
- (*dest)[type_pos]= JSONB_TYPE_LARGE_ARRAY;
- result= serialize_json_array(array, dest, true, depth);
- }
- break;
- }
这个方法会根据不同的 json 类型,执行相应的序列化逻辑。
我们本次插入的是数组,会执行 case Json_dom::J_ARRAY: 里的逻辑。
首次进来会在脚标 0 的位置设置上类型 SMALL_ARRAY,即十六进制的 02。
下面会继续进入这个方法 serialize_json_array 。
- static enum_serialization_result
- serialize_json_array(const Json_array *array, String *dest, bool large,
- size_t depth)
- {
- const size_t start_pos= dest->length();
- const size_t size= array->size();
-
-
- if (is_too_big_for_json(size, large))
- return VALUE_TOO_BIG;
- // First write the number of elements in the array.
- if (append_offset_or_size(dest, size, large))
- return FAILURE; /* purecov: inspected */
-
-
- // Reserve space for the size of the array in bytes. To be filled in later.
- const size_t size_pos= dest->length();
- // 这里只是先用2/4个字节占位, size=0,方法的最后会更新这几个字节
- if (append_offset_or_size(dest, 0, large))
- return FAILURE; /* purecov: inspected */
-
-
- size_t entry_pos= dest->length();
-
-
- // Reserve space for the value entries at the beginning of the array.
- const size_t entry_size=
- large ? VALUE_ENTRY_SIZE_LARGE : VALUE_ENTRY_SIZE_SMALL;
- // 预留 entry 的空间,元素个数 * entry_size, 先填充0
- if (dest->fill(dest->length() + size * entry_size, 0))
- return FAILURE; /* purecov: inspected */
-
-
- // 遍历 json 数组
- for (uint32 i= 0; i < size; i++)
- {
- //拿到当前数组元素
- const Json_dom *elt= (*array)[i];
- // 继续向目标字符串 dest 追加(上面已经追加到了entry),
- // start_pos 从 1 开始,第0个字节被json对象类型占用
- // entry_pos 从 5 开始, 第1、2个字节存储元素个数,第3、4个字节存储整个json数组占用的字节数
- enum_serialization_result res= append_value(dest, elt, start_pos,
- entry_pos, large, depth + 1);
- if (res != OK)
- return res;
- // 下次循环加 3
- entry_pos+= entry_size;
- }
-
-
- // Finally, write the size of the object in bytes.
- size_t bytes= dest->length() - start_pos;
- if (is_too_big_for_json(bytes, large))
- return VALUE_TOO_BIG; /* purecov: inspected */
- insert_offset_or_size(dest, size_pos, bytes, large);
-
-
- return OK;
- }
上面这段代码,会先把数组元素的个数(3个)追加到 *dest 上,就变成下图的结构。
图中灰色虚线框,为大类型占用的字节数,小类型的忽略,后面所有图中都是此效果,不再赘述。
接下来追加的是整个 json 数据占用的字节数、value_entrys,因为这 2 部分占用的字节数可以提前算出来,计算逻辑见代码注释,这里只是追加对应的字节数占位,用 0 填充,后面会逐步更新为正确的内容。就变成下图的结构。
接下来就会遍历数组里的每个元素,逐个追加。
来到方法 append_value 。
- static enum_serialization_result
- append_value(String *dest, const Json_dom *value, size_t start_pos,
- size_t entry_pos, bool large, size_t depth)
- {
- if (depth >= JSON_DOCUMENT_MAX_DEPTH)
- {
- my_error(ER_JSON_DOCUMENT_TOO_DEEP, MYF(0));
- return FAILURE;
- }
- // start_pos 从 1 开始,第0个字节被json对象类型占用
- // entry_pos 从 5 开始, 第1、2个字节存储元素个数,第3、4个字节存储整个json数组占用的字节数
- uint8 element_type;
- int32 inlined_value;
- if (should_inline_value(value, large, &inlined_value, &element_type))
- { // 追加 42、true 这两个个数组元素的时候会走到这个分支
- (*dest)[entry_pos]= element_type;
- insert_offset_or_size(dest, entry_pos + 1, inlined_value, large);
- return OK;
- }
-
-
- // 追加字符串 xy , offset = 14 - 1 = 13
- size_t offset= dest->length() - start_pos;
- if (is_too_big_for_json(offset, large))
- return VALUE_TOO_BIG;
- // dest, 9, 13, false
- // 把 offset 插入到 entry 中
- insert_offset_or_size(dest, entry_pos + 1, offset, large);
- // value, 8, dest, 1, false
- return serialize_json_value(value, entry_pos, dest, depth, !large);
- }
第1个元素是数字 42,它属于 inlined_value,类型(05)和内容(2A) 直接存储在 value_entry 中,位置是由 start_pos、entry_pos 决定。
就变成下图的结构。
第2个元素是字符串 xy,不属于 inlined_value,所以 value_entry 中会存放真实 value 的脚标位置 -1(读取的时候会读第 脚标+1 的字节)。
追加类型(0C),补充 offset 后就变成下图的结构。
追加完类型后,继续追加 value,value 会追加在当前字符串的最后,也就是 脚标 13 + 1 =14 的位置。xy 对应的十六进制是 78、79。如下图所示。
这里说一下字符的编码转换。
如果字符属于 ASCII 编码范围,就会用一个字节来存储;
如果是其他字符,采用3个字节来存储;
如果是 emoji 表情等字符,采用4个字节来存储。
之所以前面逻辑中占用字节数、offset 用0占位填充,就是当时还不能确定真实 value 占用的字节数,必须等每个 value 遍历后就知道了 offset,所有 value 遍历完后就知道了占用字节数,只需要一次循环就可以。
第3个元素是布尔值 true,属于 inlined_value,类型(04)和内容(01) 直接存储在 value_entry 中,就变成下图的结构。
到此为止,我们的 json array 就算序列化完成了。
我们看一下 facts.ibd 文件中的内容,和我们通过代码推导的是否一致。
可以看到,和我们代码推导的完全一致。
我们趁热再存一条中文的数据,["中华人民共和国"],过程不再赘述,直接看结果。
3.2 存储一个 json object
object 的序列化,整体流程和 array 类似,多了处理 key_entrys、keys 的逻辑。
插入一条 json arrsy 数据
INSERT INTO facts VALUES('{"x":33, "n":"name","ab":true, "a":"测试"} ');
注意我们插入时和查询出的 key 的顺序是不一样的,查询出的 key 显然是经过排序的,到底是插入时排的序,还是查询时排的序,这个后面代码中会看到,这里先有个印象。
同样还是跟着代码逻辑,看一下 object 的序列化过程。
还是 serialize_json_value 方法,这次是 case Json_dom::J_OBJECT: 分支。
- case Json_dom::J_OBJECT:
- {
- const Json_object *object= down_cast<const Json_object*>(dom);
- (*dest)[type_pos]= JSONB_TYPE_SMALL_OBJECT;
- result= serialize_json_object(object, dest, false, depth);
- if (result == VALUE_TOO_BIG)
- {
- // If the parent uses the small storage format, it needs to grow too.
- if (small_parent)
- return VALUE_TOO_BIG;
- dest->length(start_pos);
- (*dest)[type_pos]= JSONB_TYPE_LARGE_OBJECT;
- result= serialize_json_object(object, dest, true, depth);
- }
- break;
- }
还是先设置类型 SMALL_OBJECT 00
接下来就会进入 serialize_json_object 方法。这个方法很长,分开来讲。
- static enum_serialization_result
- serialize_json_object(const Json_object *object, String *dest, bool large,
- size_t depth)
- {
- const size_t start_pos= dest->length();
- const size_t size= object->cardinality();
-
-
- if (is_too_big_for_json(size, large))
- return VALUE_TOO_BIG;
-
-
- // First write the number of members in the object.
- // 对象内的元素个数
- if (append_offset_or_size(dest, size, large))
- return FAILURE;
-
-
- // Reserve space for the size of the object in bytes. To be filled in later.
- // 占位填充 占用的总字节数
- const size_t size_pos= dest->length();
- if (append_offset_or_size(dest, 0, large))
- return FAILURE;
上面这段代码会追加 object 内的元素个数,以及占用总字节数的占位填充,和 array 的类似,直接看结果。
接下来就是非常关键的 key_entrys 的序列化
- // 计算 key_entry、value_entry所需要的字节数
- const size_t key_entry_size=
- large ? KEY_ENTRY_SIZE_LARGE : KEY_ENTRY_SIZE_SMALL;
- const size_t value_entry_size=
- large ? VALUE_ENTRY_SIZE_LARGE : VALUE_ENTRY_SIZE_SMALL;
- /*
- Calculate the offset of the first key relative to the start of the
- object. The first key comes right after the value entries.
- {"x":33, "n":"name","ab":true, "a":"测试"}
- key_entry_size=4, value_entry_size=3,dest.length=5, 5+4(4个元素)*(4+3)-1=32=0x20
- */
- size_t offset= dest->length() +
- size * (key_entry_size + value_entry_size) - start_pos;
-
- #ifndef NDEBUG
- const std::string *prev_key= NULL;
- #endif
-
-
- // Add the key entries.
- for (Json_object::const_iterator it= object->begin();
- it != object->end(); ++it)
- {
- const std::string *key= &it->first;
- // 拿到 key 的长度
- size_t len= key->length();
-
-
- #ifndef NDEBUG
- // Check that the DOM returns the keys in the correct order.
- // 对 key 进行字典排序,先按长度排序;长度相同,再按内容排序。
- if (prev_key)
- {
- assert(prev_key->length() <= len);
- if (len == prev_key->length())
- assert(memcmp(prev_key->data(), key->data(), len) < 0);
- }
- prev_key= key;
- #endif
-
-
- // We only have two bytes for the key size. Check if the key is too big.
- // 只有2字节来存储 key 的长度,所以 key 的长度 < 2^16-1
- if (len > UINT_MAX16)
- {
- my_error(ER_JSON_KEY_TOO_BIG, MYF(0));
- return FAILURE;
- }
-
-
- if (is_too_big_for_json(offset, large))
- return VALUE_TOO_BIG;
- // 追加 key 的 offset, 追加 key 的长度
- if (append_offset_or_size(dest, offset, large) ||
- append_int16(dest, static_cast<int16>(len)))
- return FAILURE;
- offset+= len;
- }
上面这段代码,首先根据大、小类型,计算出每个 key_entry 和 value_entry 所需要的字节数。
计算规则在 mysql-5.7.44\sql\json_binary.cc 文件中的头部有如下的定义。官方注释已经写的很清楚了。
- /*
- The size of key entries for objects when using the small storage
- format or the large storage format. In the small format it is 4
- bytes (2 bytes for key length and 2 bytes for key offset). In the
- large format it is 6 (2 bytes for length, 4 bytes for offset).
- */
- #define KEY_ENTRY_SIZE_SMALL (2 + SMALL_OFFSET_SIZE)
- #define KEY_ENTRY_SIZE_LARGE (2 + LARGE_OFFSET_SIZE)
- /*
- The size of value entries for objects or arrays. When using the
- small storage format, the entry size is 3 (1 byte for type, 2 bytes
- for offset). When using the large storage format, it is 5 (1 byte
- for type, 4 bytes for offset).
- */
- #define VALUE_ENTRY_SIZE_SMALL (1 + SMALL_OFFSET_SIZE)
- #define VALUE_ENTRY_SIZE_LARGE (1 + LARGE_OFFSET_SIZE)
继续看 key_entrys 序列化的代码,紧接着就会遍历对象的元素,遍历的过程会对 key 按字典排序,然后把计算出的 key 的 offset 和 key 的长度追加,依次循环,直至所有 key 都处理完成。key 追加完成后的效果如下图所示。
排序的原因是为了更快的检索,根据 key 检索时就可以采用二分法检索。
下面会继续填充占位 value_entrys
- const size_t start_of_value_entries= dest->length();
-
-
- // Reserve space for the value entries. Will be filled in later.
- // 21+4*3=33
- dest->fill(dest->length() + size * value_entry_size, 0);
填充后的效果如下图所示(由于图片太长,分2张图展示)
接下来就是追加 key 的真实内容。
- // Add the actual keys.
- for (Json_object::const_iterator it= object->begin(); it != object->end();
- ++it)
- {
- if (dest->append(it->first.c_str(), it->first.length()))
- return FAILURE;
- }
追加后的效果
继续追加 value 的真实内容,并更新对应的 类型、inlined_value / offset
- // Add the values, and update the value entries accordingly.
- size_t entry_pos= start_of_value_entries;
- for (Json_object::const_iterator it= object->begin(); it != object->end();
- ++it)
- {
- enum_serialization_result res= append_value(dest, it->second,
- start_pos, entry_pos, large,
- depth + 1);
- if (res != OK)
- return res;
- entry_pos+= value_entry_size;
- }
追加后的效果
最后再更新整个 json object 所占用的字节数
- // Finally, write the size of the object in bytes.
- size_t bytes= dest->length() - start_pos;
- if (is_too_big_for_json(bytes, large))
- return VALUE_TOO_BIG;
- insert_offset_or_size(dest, size_pos, bytes, large);
-
-
- return OK;
- }
来一张全图看看效果。
4.1 value 的 size 为什么采用变长的形式存储?读的时候怎么知道要读几个字节?
问题涉及到的是这段代码
- case Json_dom::J_STRING:
- {
- const Json_string *jstr= down_cast<const Json_string*>(dom);
- size_t size= jstr->size();
- if (append_variable_length(dest, size) ||
- dest->append(jstr->value().c_str(), size))
- return FAILURE;
- (*dest)[type_pos]= JSONB_TYPE_STRING;
- result= OK;
- break;
- }
- static bool append_variable_length(String *dest, size_t length)
- {
- do
- {
- // Filter out the seven least significant bits of length.
- uchar ch= (length & 0x7F);
-
-
- /*
- Right-shift length to drop the seven least significant bits. If there
- is more data in length, set the high bit of the byte we're writing
- to the String.
- */
- length>>= 7;
- if (length != 0)
- ch|= 0x80;
-
-
- if (dest->append(ch))
- return true; /* purecov: inspected */
- }
- while (length != 0);
-
-
- // Successfully appended the length.
- return false;
- }
个人觉得可以根据元素的个数,调整一下存储结构。
如果元素个数少,可以固定 4 个字节存储 size,有浪费也不会太多,省去了存、取 时的移位比较操作。
如果元素多,还采用这种变长存储结构,可以节省不少空间。
4.2 json object 存储要 3 次遍历,是否能减少遍历次数?
这段代码在前面已经讲的很详细了。
个人觉得也可以分场景,第 2 次遍历需要的内容可以在第 1 次遍历时缓存,以空间换时间。
扯两句
好的存储结构,就要抠每一个字节。
一旦数据量大了,就得研究底层的存储结构。合理设计业务内容的长度可能成倍的节省存储空间,因为除了内容本身还要有很多空间来支撑这个数据结构。
原创不易,多多关注,一键三连,感谢支持!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。