CJSON: внутренний формат Reindexer для обработки данных
В Reindexer используется внутренний формат представления данных CJSON
(Compact JSON).
С его помощью решаются задачи:
- Хранение -
-tuple
(используется в ядре Reindexer совместно с со структуройPayloadValue
). - Передача данных по сети - «транспортный»
CJSON
.
Структура CJSON
для обоих случаев схожая, однако между -tuple
и «транспортным» CJSON
есть различия в способах хранения значений полей документа.
Структура CJSON
Каждое поле в СJSON
представлено в виде ctag
, который кодирует тип, имя и двоичное представление данных поля в формате, зависящем от типа.
Формат ctag
представлен в таблице:
Поле ctag |
Размер в битах | Описание |
---|---|---|
TypeTag0 | 3 | Тип поля. Зависит от типа данных в поле. Записывается в виде TAG_XXX . Возможные значения представлены ниже |
NameIndex | 12 | Индекс имени поля в словаре имён. 0 означает пустое имя |
FieldIndex | 10 | Индекс поля данных в PayloadValue . 0 - нет ссылки, значение следует сразу за ctag |
Reserved | 4 | Зарезервировано для дальнейшенго использования |
TypeTag1 | 3 | Дополнительные старшие биты для Типа поля. Совместно с TypeTag0 определяют фактический тип данных |
Возможные значения поля TypeTag
:
Тип поля (TypeTag) | Значение | Описание |
---|---|---|
TAG_VARINT | 0 | Данные в целочисленном формате |
TAG_DOUBLE | 1 | Числа с плавающей точкой |
TAG_STRING | 2 | Данные в формате строки |
TAG_ARRAY | 3 | Данные — массив элементов |
TAG_BOOL | 4 | Данные в формате boolean |
TAG_NULL | 5 | Null |
TAG_OBJECT | 6 | Данные — объект |
TAG_END | 7 | Конец CJSON-объекта |
TAG_UUID | 8 | Данные в формате UUID. Старший бит хранится в TypeTag1 |
Хранение массивов
Массивы могут храниться двумя различными способами:
- однородный массив из элементов одного типа,
- смешанный массив из элементов разных типов.
Оба типа массивов в дополнение к ctag
используют atag
.
atag
— формат тега массива
atag
— тег (4 байта, int), с помощью которого кодируется количество и тип элементов массива:
Поле тега | Размер в битах | Описание |
---|---|---|
Count | 24 | Количество элементов в массиве |
TypeTag | 6 | Тип элементов массива. Если TypeTag массива равен — TAG_OBJECT , массив является смешанным, и каждый его элемент содержит свой ctag . TypeTag здесь может принимать те же значения, что и в ctag |
Формат пакета CJSON
# | Поле | Описание |
---|---|---|
1 | Offset to names dict | Смещение словаря имен (номер байта в пакете, с которого начинается словарь). Если в пакете нет нового словаря имен, TAG_END в начале отсутствует |
2 | Records | Дерево записей. Начинается с TAG_OBJECT и заканчивается TAG_END |
3 | Names dictionary | Словарь имен полей |
names_dictionary :=
<varint(count)>
[[<varint(string length)>, <name char array>],
[<varint(string length)>, <name char array>],
...
Offset to names dict
не всегда присутствует в пакете. Если пакет начинается с TAG_END
, за ним размещается Offset to names dict
. Если же пакет начинается не с TAG_END
, то в нем вообще не содержится словарь имен.
Формат записи:
record :=
ctag := (TAG_OBJECT,name)
[field :=
ctag := (TAG_VARINT,name) data := <varint> |
ctag := (TAG_DOUBLE,name) data := <8 byte double> |
ctag := (TAG_BOOL,name) data := <1 byte: 0 - False, 1 - True> |
ctag := (TAG_STRING,name) data := <varint(string length)>, <char array> |
ctag := (TAG_NULL,name) |
ctag := (TAG_ARRAY,name)
data := atag := (TAG_OBJECT|TAG_VARINT|TAG_DOUBLE|TAG_BOOL, count)
array := [ctag := TAG_XXX] <data>,[[ctag := TAG_XXX>]<data>]] ... |
ctag (TAG_OBJECT,name)>
[subfield := field]
...
ctag := (TAG_END)
]
...
ctag := (TAG_END)
Пример объекта CJSON
{
"name": "Hello",
"year": 2010,
"articles": [
1,
2,
3,
4,
5
],
"info": {
"name": "Info"
}
}
(TAG_OBJECT) 06
(TAG_STRING,1) 5,"Hello" 0A 05 48 65 6C 6C 6F
(TAG_VARINT,2) 2010 10 B4 1F
(TAG_ARRAY,3) (TAG_VARINT,5) 1 2 3 4 5 1B 05 00 00 00 01 02 03 04 05
(TAG_OBJECT,4) 26
(TAG_STRING,1) 4,"Info" 0A 04 49 6E 66 6F
(TAG_END) 07
(TAG_END) 07
1 - "name"
2 - "year"
3 - "articles"
4 - "info"
-tuple
и «транспортный» CJSON
-tuple
-tuple
в формате CJSON
используется для обработки документов ядром Reindexer.
Его применение позволяет решить проблему с отсутствием унифицированной структуры записей в документоориентированных БД, когда каждая запись может иметь уникальные поля и нужно где-то хранить информацию по ее неиндексным полям.
-tuple
- всегда самый первый (с индексом 0) элемент записи (PayloadValue -> Item).
В нем хранятся либо ссылки на индексные поля записи, либо сами значения — в случае если поле неиндексное.
В CJSON
каждое поле записи (документа, строки неймспейса) описывается с помощью ctag
(и carraytag
).
В ctag
хранится:
- тип поля (
TypeTag
), - целочисленные номер имени поля (
NameIndex
), - индекс поля в
PayloadValue
(FieldTag
) - если поле индексное .
Если поле индексное, в FieldTag
хранится его индекс в PayloadType
и IndexesStorage
(этот индекс также позволяет вычислить его смещение в PayloadValue
).
При этом в -tuple
сериализуется только ctag
, а само значение находится в PayloadValue
(доступ к нему можно получить через метод PayloadIface::Get(ctag.field)
).
Если поле неиндексное, значение ctag.field
= -1.
Значение поля сериализуется сразу за ctag
.
Все документы неймспейса хранятся в NamespaceImpl
(массив items_
) в виде PayloadValue
.
У каждого такого документа в первом поле находится значение -tuple
.
«Транспортный» CJSON
«Транспортный» CJSON
используется для отправки данных по сети.
С ним работают cproto-клиенты, grpc-клиенты, rpc/grpc-серверы.
Так же он используется при проксировании в синхронном кластере и шардировании.
В «транспортным» CJSON
(в отличие от -tuple
, используемого ядром) каждое поле, независимо от того, индексное оно или неиндексное, кодируется без ссылок на его значение в PayloadValue
.
То есть значение каждого поля хранится непосредственно внутри объекта CJSON
.
Ссылки на индексные поля в данном случае использоваться не могут, поскольку клиент может не иметь данных PayloadType
.
Пример обхода структуры CJSON
(код из skipCjsonTag):
void skipCjsonTag(ctag tag, Serializer &rdser) {
const bool embeddedField = (tag.Field() < 0);
switch (tag.Type()) {
case TAG_ARRAY: {
if (embeddedField) {
carraytag atag = rdser.GetUInt32();
for (int i = 0; i < atag.Count(); i++) {
ctag t = atag.Tag() != TAG_OBJECT ? atag.Tag() : rdser.GetVarUint();
skipCjsonTag(t, rdser);
}
} else {
rdser.GetVarUint();
}
} break;
case TAG_OBJECT:
for (ctag otag = rdser.GetVarUint(); otag.Type() != TAG_END; otag = rdser.GetVarUint()) {
skipCjsonTag(otag, rdser);
}
break;
default:
if (embeddedField) rdser.GetRawVariant(KeyValueType(tag.Type()));
}
}
Основные модули работы с CJSON
В Reindexer для работы с CJSON
используются следующие основные модули:
Модуль | Описание | Ссылка |
---|---|---|
CJsonModifier |
Класс модификации существующего CJSON. Используется для обновления и удаления существующих, добавления новых (как индексных, так и неиндексных) полей. Поиск поля для модификации осуществляется по его TagsPath (json path с тэгом на каждое вложенное поле) через рекурсивный обход json-подобной структуры CJSON. Обновление поля происходит через построение нового CJSON, либо с обновлением текущих полей (или добавлением новых), либо с пропуском (при удалении) некоторых из них. Если при обходе всего CJSON соответствующее поле не было найдено, осуществляется вставка нового поля из корня cjson в соответствие с заданным json path |
https://github.com/Restream/reindexer/blob/master/cpp_src/core/cjson/cjsonmodifier.cc |
ItemModifier |
Класс модификации записи БД (Item ) при выполнении Update запроса. Модифицирует PayloadValue существующей записи на основе UpdateEntry (из объекта Query ). Обновление полей-объектов происходит через модификацию CJSON (построение нового CJSON) c использованием класса CJsonModifier и последующим обновлением всех индексных полей (включая композитные индексы). В случае обновления скалярных полей (полей не объектов) происходит либо обновление индексов, либо обновление CJSON через CJsonModifier |
https://github.com/Restream/reindexer/blob/master/cpp_src/core/itemmodifier.cc |
BaseEncoder |
Класс построения выходных форматов данных (json/msgpack/cjson/protobuf) на основании -tuple существующей записи. Через BaseEncoder происходит конвертация представления данных Item в JSON или транспортный CJSON. Алгоритм построения выходных форматов основан на рекурсивном обходе json-подобной структуры -tuple с записью данных в выходной WrSerializer поток с использованием Builder (JSONBuilder/CJSONBuilder/MsgPackBuilder/ProtobufBuilder/CsvBuilder) объекта, переданного пользователем как аргумент в метод Encode |
https://github.com/Restream/reindexer/blob/master/cpp_src/core/cjson/baseencoder.cc |
CJsonDecoder |
Класс, декодирующий tuple/cjson с последующей записью выходных данных в WrSerializer поток в формате -tuple . Алгоритм работы осуществляется на основе рекурсивного обхода json-подобной структуры CJSON |
https://github.com/Restream/reindexer/blob/master/cpp_src/core/cjson/cjsondecoder.cc |
CJsonBuilder |
Класс построения полей CJSON. CJsonBuilder используется BaseEncoder для построения транспортабельного CJSON. Каждое поле кодируется полностью: ctag (поле field = -1) + значение, следующее за ним |
https://github.com/Restream/reindexer/blob/master/cpp_src/core/cjson/cjsonbuilder.cc |
FieldsExtractor |
Класс извлечения значений неиндексных полей из CJSON объекта. Поиск и получение значений полей основаны на работе класса BaseEncoder, который использует FieldsExtractor как объект Builder при рекурсивном обходе -tuple . Поле считается найденным, когда значение внутренней переменной expectedPathDepth_ (начальное значение равно длине json path) становится равным нулю (означает правильный уровень вложенности поля) |
https://github.com/Restream/reindexer/blob/master/cpp_src/core/cjson/fieldextractor.h |
CJson tools |
Набор функций для работы с CJSON: генерация -tuple на основании PayloadValue , копирование значения поля в CJSON, создание поля-ссылки (индексное поле в -tuple ) и неиндексного поля (как неиндексное поле в -tuple , так и поле в «транспортном» CJSON) и т.д. |