TDR Blob 内嵌 Protobuf 部分字段读写

当 TDR 表的 Blob 中保存了 Protobuf(下文简称 PB)数据时,可以通过字段路径,只读取或更新其中需要的内容。例如,只修改路由规则中的实例地址,无需在客户端读取、解析并回写整个 Blob。表本身仍然是 TDR 表。

PB 的二进制编码是半自解释的:每个字段带有字段编号和 wire type,解析方可以据此确定字段数据的边界;其中字符串、bytes、message 等使用带长度的编码。Tcaplus 利用这些信息,按路径提取或替换所选内容,保留未选中的内容。编码格式可参阅 Protobuf 官方编码说明

PB 数据与定义的兼容性需要业务自行保证。 服务端知道 TDR 表结构,但不持有 Blob 内业务数据的 .proto 定义。wire type 并不等于完整的业务类型,也不能单独说明 message 层级、map/repeated 或 oneof 关系。存量数据、本次写入的数据、构造路径和解析结果所用的 PB 定义必须兼容;路径转换成功不代表历史数据一定符合当前定义。已上线的字段编号不能随意改变或复用,其他类型变更也应遵守 Protobuf 的消息演进规则

下面先用一组 C++ 示例混合选择常见类型的路径,并给出简短的 Go 对照,再展开说明路径规则。完整接入流程见 C++ SDK 示例Go SDK 示例

1. 综合读写示例

1.1 示例数据与准备

本功能适用于 TDR Generic 表,支持单条部分读取、单条部分更新和批量部分读取,不适用于 List/SortList 表。服务端和 SDK 均须支持该功能;C++ 使用提供 tcaplus_tdr_pb_fields.h 的 Service API,Go SDK 的 v0.6.36 已包含本文使用的接口。

沿用 service_info.xml 中的 routeinfo Blob 和原生 TDR 字段 filterdata

<entry name="routeinfo_len" type="uint" defaultvalue="0" desc="路由规则信息长度" />
<entry name="routeinfo" type="char" count="1024" refer="routeinfo_len" desc="路由规则信息" />

routeinfo_len 是 Blob 的有效字节数,1024 是数组容量。Blob 中存放以下普通 PB message,无需创建 PB 表或添加 Tcaplus PB 表选项:

syntax = "proto3";
package routeinfo;

message RouteInfo {
  uint32 version = 1;
  string strategy = 2;
  Weight weight = 3;
  repeated string tags = 4;
  repeated Instance instances = 5;
  map<int64, Instance> instance_map = 6;
  map<string, string> settings = 7;
}
message Weight { uint32 cpu = 1; uint32 memory = 2; }
message Instance { string addr = 1; uint32 port = 2; uint32 weight = 3; }

假定客户端已初始化并注册该表,主键为 ("dev", "oa", "com") 的记录已存在,Blob 中是合法的 RouteInfo 编码,且至少有一个 instances 元素、instance_map[1001]settings['region']。这些前提与 Go SDK 初始数据示例一致。

C++ 代码使用生成的 SERVICE_INFOrouteinfo::RouteInfo,并包含 tcaplus_tdr_pb_fields.h。下文省略客户端初始化、主键填写、请求创建和响应接收循环,完整流程见 C++ SDK 示例

1.2 混合选择路径并读取

先创建 TCAPLUS_API_PB_FIELD_GET_REQ 请求、添加记录,并通过 SetData 设置带主键的 TDR 数据。然后将需要的 PB 字段名和原生 TDR 字段放在同一组请求中:

const char* pb_fields[] = {
    "version",                  // 普通字段
    "weight.cpu",               // 嵌套 message 子字段
    "tags",                     // 整个 repeated 字段
    "instances[0].addr",        // repeated 元素的子字段
    "instance_map[1001].port",   // 整数 key 的 map 子字段
    "settings['region']"        // 字符串 key 的 map value
};
const TcaplusService::TdrPbFieldGroup field_groups[] = {
    TcaplusService::TdrPbFieldGroup(
        "routeinfo", routeinfo::RouteInfo::descriptor(), pb_fields),
    TcaplusService::TdrPbFieldGroup("filterdata") // 原生 TDR 一级 value 字段
};
int32_t ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);
if (ret != TcapErrCode::GEN_ERR_SUC) return ret;
return server.SendRequest(request);

TdrPbFieldGroup 的三个参数依次是 TDR Blob 路径、PB 类型的 descriptor、相对 PB 根 message 的字段名列表。descriptor 只提供类型信息,不携带读写数据;多个 Blob 可以分别设置分组。这种字段名写法称为 name path,名称取 .proto 中的原名,不是 JSON name 或 C++/Go 生成的成员名。

收到响应后,依次检查 GetResultFetchRecordGetData 的结果。下面假定返回的 TDR 记录已取到 row 中,按有效长度解析部分 PB:

if (row.dwRouteinfo_len > sizeof(row.szRouteinfo)) {
    return TcapErrCode::API_ERR_OVER_MAX_FIELD_VALUE_LEN;
}
routeinfo::RouteInfo partial;
if (!partial.ParsePartialFromArray(row.szRouteinfo, static_cast<int>(row.dwRouteinfo_len))) {
    return TcapErrCode::API_ERR_UNPACK_MESSAGE;
}
// 从 partial 读取所选 PB 字段,从 row.szFilterdata 读取原生 TDR 字段。

结果保留 PB 的 message 层级,只包含选中的内容。未返回的 strategyweight.memory 等字段不代表原记录中为空;返回的 refer 也是部分 PB 的长度。不能将部分结果直接用于完整 Blob 覆盖。

1.3 用同一组路径更新

创建新的 TCAPLUS_API_PB_FIELD_SET_REQ 请求并添加记录,填写相同主键,复用上述 field_groups。下面为每条选中路径准备新值,并设置原生 TDR 字段:

routeinfo::RouteInfo delta;
delta.set_version(2);
delta.mutable_weight()->set_cpu(80);
delta.add_tags("blue");
delta.add_tags("green");
delta.add_instances()->set_addr("10.0.0.99");
(*delta.mutable_instance_map())[1001].set_port(9090);
(*delta.mutable_settings())["region"] = "gz";
snprintf(row.szFilterdata, sizeof(row.szFilterdata), "%s", "enabled");

const size_t encoded_size = delta.ByteSizeLong();
if (encoded_size > sizeof(row.szRouteinfo)) {
    return TcapErrCode::API_ERR_OVER_MAX_FIELD_VALUE_LEN;
}
if (!delta.SerializePartialToArray(row.szRouteinfo, static_cast<int>(encoded_size))) {
    return TcapErrCode::API_ERR_PACK_MESSAGE;
}
row.dwRouteinfo_len = static_cast<uint32_t>(encoded_size);
int32_t ret = record->SetData(&row, sizeof(row));
if (ret != TcapErrCode::GEN_ERR_SUC) return ret;
ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);
if (ret != TcapErrCode::GEN_ERR_SUC) return ret;
return server.SendRequest(request);

请求由“主键 + 字段路径 + 增量 PB”组成:路径决定修改范围,增量提供新值,这里的“增量”不是数值自增。此次更新将 tags 整体替换为两项,只修改第一个实例的地址及 map 中指定的内容;strategyweight.memory、实例的其他字段和其他 map key 保留。选中的字段若未提供新值,可能表示清空,详见第 3 节。

该接口只更新已有记录,记录不存在或已过期时不会自动插入。发送成功后仍须检查服务端响应。

1.4 Go 对照

Go 使用 BuildPaths 构造同一组路径,再交给同步接口。以下 client 已初始化,data 已填好主键,TableName 指向示例表:

paths, err := tdrpb.BuildPaths(
    tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil),
        "version", "weight.cpu", "tags", "instances[0].addr",
        "instance_map[1001].port", "settings['region']"),
    tdrpb.Raw("filterdata"),
)
if err != nil {
    return
}
if err = client.DoPBFieldGet(TableName, data, &option.TDROpt{FieldNames: paths}); err != nil {
    return
}
partial := &routeinfo.RouteInfo{}
if err = tdrpb.UnmarshalPartial(data.Routeinfo, data.Routeinfo_Len, partial); err != nil {
    return
}

(*routeinfo.RouteInfo)(nil) 只提供类型信息。更新时构造与 C++ 示例相同的增量,用 tdrpb.MarshalPartialMarshalPartialTo 写入 data.Routeinfo、填写 Routeinfo_LenFilterdata,再调用 client.DoPBFieldUpdate(TableName, data, &option.TDROpt{FieldNames: paths})。完整代码见 Go SDK 示例

2. 字段名接口与 SetFieldNames

SetFieldNames 原来用于选择 TDR 一级字段,如 filterdatarouteinfo。本功能将选择范围扩展到 Blob 内嵌的 PB 字段,因此 PB 路径可以与原生 TDR 一级 value 字段混用。嵌套 PB 路径须配合本功能的部分读写接口使用,普通 TDR Get/Update 的字段选择规则不变。

C++ 的 SetAllTdrPbFieldNames 封装了字段名转换和 SetFieldNames 调用。Go 的 BuildPaths 返回转换后的列表,将其赋给 TDROpt.FieldNames 后,由 DoPB* 接口构造请求并调用 SetFieldNames。字段名转换本身不发送请求。

SetFieldNames 中的路径保留 TDR 字段名,进入 PB 后使用 .proto 等号右侧的字段编号,称为 tagid path。综合示例的对应关系如下:

选择的字段 完整 tagid path
version routeinfo.1
weight.cpu routeinfo.3.1
tags routeinfo.4
instances[0].addr routeinfo.5#[0].1
instance_map[1001].port routeinfo.6[1001].2
settings['region'] routeinfo.7['region']
原生 TDR 字段 filterdata filterdata

需要直接指定字段编号时,C++ 可调用 int32_t SetFieldNames(const char* field_name[], unsigned field_count);Go 可把对应列表赋给 TDROpt.FieldNames,或直接调用 Request 的 SetFieldNames([]string) errortdrpb.Raw 也可原样传入完整 tagid path,不做字段名转换。SetFieldNames 不接收 routeinfo.version 这样的 PB 字段名路径。

直接构造请求时,应先初始化请求、添加记录并 SetData,再设置字段路径,检查各步返回值。SDK 自动补齐承载 PB 的一级 TDR 字段及所需 refer,无需手动加入 routeinforouteinfo_len;手动同时选择 routeinforouteinfo.1 会产生父子路径冲突。

3. 路径规则与读写语义

3.1 路径终点决定读写范围

路径结束在子字段、message 或集合上,操作范围也随之变化。以下普通路径均为 PB name path:

选择的路径 增量内容 更新结果
version 空 PB 清除 version 编码,本例 proto3 解析为 0
weight.cpu weight: {cpu: 80} 只修改 cpu,memory 保留
weight weight: {cpu: 80} 替换整个 weight,memory 不保留
tags tags: ["blue", "green"] 替换整个数组,不是追加
instance_map[1001] map 中 key 1001 的新 value 替换或新增该元素,其他 key 保留
instance_map map 中仅含 key 1001 替换整个 map,其他 key 不保留

普通标量、message 或整个集合被列入路径,但增量中没有该字段,表示清除。proto3 零值、空字符串等可能不写入编码,因此路径列表也表达了清空意图。未选中的字段保留,包括未选中的未知字段;整体替换 message、集合或 Blob 时,须提供其中所有希望保留的内容。map 指定 key 的更新和删除另有规则,见下文。

读取普通字段时,若记录中没有该字段的编码,可以成功返回不含该字段的部分 PB;解析后的默认值由客户端定义决定。指定的 map key 不存在或 repeated 下标越界则返回错误,不等同于读到默认值。

3.2 repeated 下标与返回位置

name path 用 instances[0] 选择完整元素,用 instances[0].addr 继续选择其子字段。tagid path 对应 routeinfo.5#[0]routeinfo.5#[0].1。下标从 0 开始,-1 表示最后一个元素,例如 instances[-1].addr

tagid path 用 #[] 表示 repeated 下标,用 [] 表示 map key。 PB 的 map 在编码上相当于 repeated entry message;服务端没有业务 .proto,无法仅凭编码区分两者,因此需要 #。字段名接口根据 PB 类型自动转换;手写 routeinfo.5[0] 会被解释为按 map key 访问。

按下标访问仅适用于非 packed repeated,如字符串和 message 数组。更新时目标下标必须存在,增量数组只放一个待写元素,无需补齐前面的占位项。即使将综合示例的路径改为 instances[3].addr,增量仍只需一次 add_instances();更新的是原数组第 4 个元素。

读取结果中的数组紧凑排列:读取 instances[3].addr,结果位于返回 PB 的 instances[0].addr。同一 repeated 不支持在一次请求中更新多个不同下标,也不支持按下标追加或删除;需要整体更新时选择 instances 并提供完整新数组。

3.3 map 更新、key 与删除

更新 instance_map[1001].port 只修改该 value 的 port;选择 instance_map[1001] 则替换完整 value,key 不存在时新增。增量 map 必须包含同一个 key,缺少它会返回 COMMON_ERR_ELEMENT_NOT_EXIST,不会隐式删除元素。

按 key 访问支持 stringint32int64uint32uint64。整数使用不带引号的十进制数,字符串使用单引号或双引号;整数 1001 与字符串 '1001' 不等价。字符串还可用十六进制字节表示,name path 和 tagid path 均支持:

key 写法 含义
['region'][0x726567696f6e] 同一个字符串 region
[0x610062] 三个字节 61 00 62,即 a、零字节、b
[0x31] 字符串 1,不同于整数 [1] 和字符串 ['0x31']
[''] 空字符串

十六进制支持 0x/0X 和大小写数字,奇数位在开头补零,如 0xabc 等价于 0x0abc;裸 0x 或非十六进制字符不合法。0x610x6100 是不同的 key。该写法不改变 PB string 的编码约束。

删除整个 map 元素使用 POP。为新的 TCAPLUS_API_PB_FIELD_SET_REQ 请求设置主键和空增量 Blob(refer 为 0)后,设置删除路径:

const char* field_names[] = {"POP routeinfo.6[1001]"};
int32_t ret = request->SetFieldNames(field_names, 1);
if (ret != TcapErrCode::GEN_ERR_SUC) return ret;

发送请求即可删除 key 1001,其他 key 保留;删除不存在的 key 也成功。Go 使用 tdrpb.Pop("routeinfo", (*routeinfo.RouteInfo)(nil), "instance_map[1001]") 分组,交给 BuildPaths 后得到相同路径。

POP 是路径列表中的指令,不通过 SetOperationopt.Operation 传递。普通更新直接填写路径,也接受显式的 SET routeinfo.1。Go 的 Blob 中只填写字段名路径,删除使用 Pop,不在字段名中拼接操作符。

4. 限制与进阶用法

4.1 字段选择、容器层数与嵌套 Blob

字段列表不能为空,不能包含 TDR key 字段。同一请求不能重复选择同一路径,也不能同时选择父路径与子路径:

tagid path 组合 冲突原因
routeinforouteinfo.1 完整 Blob 与其子字段重叠
routeinfo.6routeinfo.6[1001] 完整 map 与其元素重叠
routeinfo.7['region']routeinfo.7[0x726567696f6e] 同一个 key
routeinfo.6[1001]POP routeinfo.6[1001] 同一元素同时更新和删除

一条 PB 路径最多访问一层容器元素,如 instance_map[1001].port。如果该 value 内还有 map/repeated,不能在同一路径上再次选择其中的元素;可以读取 value,修改后整体替换,并配合版本检查。

Blob 也可以位于 TDR 嵌套结构体中。假设 extra.bin 同样保存 RouteInfo,将分组的 Blob 前缀改为 extra.bin,选择 version 就会生成 extra.bin.1。原生 TDR 结构体子字段(如 extra.tag)不支持独立选择,需要整体读写一级字段 extra

4.2 PB 类型与兼容性

项目 规则
packed repeated 只能按完整字段读取或覆盖;没有 proto 时无法可靠识别编码段中的元素类型和业务下标
map key 类型 boolsint*fixed*sfixed* 等不支持按 key 访问,必要时整体读写 map
oneof 不自动清除同组其他成员。切换时将旧成员一并列入路径清空,或整体替换包含 oneof 的 message
proto2 required 部分数据可能缺 required 字段,须使用 partial 编解码;服务端不验证最终数据是否满足 required 约束
proto2 默认值 清除字段编码后 has_xxx() 为 false,读取值取客户端定义的默认值,不保证为 0
自增及更新表达式 不支持 PB 字段自增请求,也不支持通过独立 SetOperation/opt.Operation 添加更新表达式;map 删除使用路径 POP

C++ 使用 SerializePartialToArrayParsePartialFromArray 等 partial 接口并检查结果。Go 可使用 tdrpb.MarshalPartialMarshalPartialToUnmarshalPartial;Blob 生成为 []int8 时使用对应的 Int8 函数。TDR 记录使用配套的 SetData/GetData,参见 TDR 表数据读写与版本兼容性

4.3 数量与容量

项目 限制
单条路径 最多 1023 字节,含操作前缀和空白,另保留结尾 \0
单请求字段配额 最多 256 项,包括 SDK 自动补齐、去重后的一级字段及独立 refer
批量读取 单次最多 1024 条主键
条件文本 最多 1023 字节,并受所用 SDK 的条件支持范围约束
Blob 大小 增量和合并后的完整 PB 均须满足 TDR 数组容量及 refer 类型范围

综合示例的 7 条路径,加上自动补齐的 routeinforouteinfo_len,共占 9 项;单独选择 extra.bin.1 时补齐一级结构体 extra,共占 2 项。不要仅按叶子路径数量估算配额。

增量小不代表最终 Blob 一定能写入:更长的新值可能使合并数据超过容量。若 refer 为 uint8,即使数组更大,也最多表示 255 字节。

4.4 repeated 范围读取

非 packed repeated 可用 tagid path routeinfo.5#[0-1] 读取前两个元素的完整内容。左右边界均包含在内,且须位于已有元素范围内;不支持范围更新。

C++ 直接将该路径交给 SetFieldNames。当前 Go name path 不支持范围转换,可通过 tdrpb.Raw("routeinfo.5#[0-1]") 或直接填写 TDROpt.FieldNames 传入。

4.5 批量读取、条件、版本与响应

操作 C++ Service API 请求类型 Go 同步接口
单条部分读取 TCAPLUS_API_PB_FIELD_GET_REQ DoPBFieldGet
单条部分更新 TCAPLUS_API_PB_FIELD_SET_REQ DoPBFieldUpdate
批量部分读取 TCAPLUS_API_PB_BATCH_FIELD_GET_REQ DoPBBatchFieldGet

批量读取对所有主键使用同一组路径。C++ 检查响应 GetResult 和每次 FetchRecord 的结果;Go 通过 BatchResultBatchVersion 返回逐条结果和版本,即使接口返回错误,也应继续检查已返回的逐条结果。

条件作用于记录当前数据,条件字段不必出现在选择列表中。例如 routeinfo.1 < 100 AND STRING(routeinfo.2) = 'round_robin'。条件中的 PB 路径使用 tagid;SetAllTdrPbFieldNamesBuildPaths 不转换条件表达式。单条读取和更新均支持条件;C++ 批量读取支持条件,当前 Go 批量读取不支持。条件不成立返回 COMMON_ERR_CONDITION_NOT_MATCHED,不执行更新,语法及类型约束见 条件过滤和更新语法说明

根据旧值计算新值、整体覆盖 message/集合时,应使用读到的正版本号并启用版本匹配检查。冲突返回 SVR_ERR_FAIL_INVALID_VERSION,需要重新读取后判断是否重试。部分更新不能防止同一字段被并发覆盖;Go 的读取响应会回写 opt.Version,复用选项更新时须明确是否需要版本检查。自动递增策略下,删除不存在的 map key 也会作为成功更新递增版本。

更新响应可选择不返回字段、回显请求、返回更新后完整记录或更新前完整记录。回显的增量不等于合并后的完整 Blob,详见 ResultFlag 响应控制与 SDK 示例。网络超时不能证明更新未执行,应按业务幂等性要求重试或读取确认。

5. SDK 示例与相关文档

  • C++ SDK 示例TdrPbFieldGroupSetAllTdrPbFieldNames 的接口定义,单条与批量请求、响应处理,以及完整示例的文件位置和编译运行入口。
  • Go SDK 示例:初始数据、增量构造、单条和批量调用、选项及错误码。动态 PB 类型可使用 BlobDesc/PopDesc
  • 部分字段查询和更新:普通 TDR 接口、PB 表及本功能的入口。
  • TDR 表数据读写与版本兼容性:TDR 编解码和元数据兼容要求。
  • 错误码文档:参数错误、记录或元素不存在、条件及版本不匹配等错误的处理。

results matching ""

    No results matching ""