ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

简单数据库实现——Part10:用 TaoToken 统一 Key 调试 B 树叶节点分裂

简单数据库实现——Part10:用 TaoToken 统一 Key 调试 B 树叶节点分裂 1. 从零实现 B 树时叶节点分裂到底难在哪如果你正在跟着「简单数据库实现」系列手写 B 树Part10 大概率是你第一次真正感到「树」这个概念压过来的地方。前面几节里整棵树只有一个节点插入就是往数组尾巴上塞.btree打印出来永远是一行leaf (size N)。到了这一节叶节点满了要一分为二还要凭空造出一个内部节点当父节点指针、页号、最大键、根标记全都要对上任何一处错位都会让后续查找直接崩掉。这一篇聚焦的就是这个调试场景叶节点分裂前后节点结构到底长什么样内部节点的指针有没有更新正确。我会给出可复制的config.toml与settings.json配置骨架把 AI 辅助排查接到统一的 Key/API 通道上然后完整演示一次「插入第 15 行触发分裂」的验证动作。目标很明确——你照着做完能独立复现叶节点分裂并用.btree的输出确认内部节点指针指向了正确的子页。适合谁看已经写完 Part9、手里有一个能跑的单节点 B 树、正准备实现leaf_node_split_and_insert和create_new_root的人。如果你还没到这一步建议先把前面的插入和游标逻辑跑通否则分裂调试会变成盲人摸象。先说清楚一个容易混淆的点。叶节点分裂不是「把数组切成两半」这么简单它同时涉及三件事旧页保留左半、新页接收右半、父节点或新根记录「左子最大键 右子页号」。第三件事最容易漏因为前两件事在内存里看得到第三件事只体现在内部节点的字节布局里。调试的核心就是让第三件事变得可见。2. 前置准备用 TaoToken 统一 Key 打通 AI 排查通道调试 B 树这种「结构对不上就全盘错」的问题光靠 printf 有时候不够你往往需要一个能读懂你贴的节点 dump、帮你比对指针的助手。问题在于不同 AI 工具的 Key 和接入地址各管各的切来切去很烦。我的做法是用 TaoToken 把 Key 和 API 通道统一起来一个 Key 走多个工具。TaoToken 在这里扮演的角色是统一的模型接入层你拿到一个 Key配置好 base URL就能在命令行工具、编辑器插件、脚本里共用同一套凭证。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 这条不带 UTM 参数配置时直接写这个。具体要准备的东西不多一个可用的 API Key在控制台的 API Keys 页面创建地址 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 以及你本地已经能编译运行的 B 树项目。Key 创建后先别急着到处贴下面用环境变量注入避免硬编码进仓库。如果你更习惯在对话界面里贴节点 dump 让模型帮你分析可以直接用模型对话入口 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 如果你打算把排查逻辑写进脚本、长期跑编码任务那更适合用 Coding Plan入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 配置项对不上时以文档为准。注意Key 只放在本地环境变量或未提交的配置文件里不要写进会 push 的源码。下面两份配置骨架都假设你从环境变量读取。3. 可复制配置config.toml 与 settings.json 骨架先给config.toml适合命令行工具或脚本读取。核心是 base URL 指向 TaoToken 的 API 地址模型名按你实际开通的填超时给足因为贴大段节点 dump 时响应会慢一些。# config.toml [provider] name taotoken base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY # 从环境变量读取不写明文 timeout_seconds 120 max_retries 2 [model] name your-model-name # 按控制台实际可用模型填写 temperature 0.2 # 排查场景要稳定别太高 max_tokens 4096 [debug] dump_node_on_error true # 出错时自动打印节点结构 print_tree_depth 3 # .btree 递归打印层数再给settings.json适合编辑器插件或图形化工具。字段名按你用的工具微调但 base URL 和 Key 来源这两项是固定的。{ provider: taotoken, baseUrl: https://taotoken.net/api, apiKeyEnv: TAOTOKEN_API_KEY, model: your-model-name, request: { timeout: 120, retries: 2, temperature: 0.2 }, debug: { dumpNodeOnError: true, printTreeDepth: 3 } }两份配置的对照关系可以用一张表说清楚方便你在不同工具间迁移配置项config.tomlsettings.json作用接入地址base_urlbaseUrl统一指向 TaoToken API凭证来源api_key_envapiKeyEnv从环境变量读 Key模型model.namemodel指定排查用模型超时timeout_secondsrequest.timeout贴大 dump 时给足节点 dumpdump_node_on_errordebug.dumpNodeOnError出错自动打印结构设置环境变量的命令Linux/macOS 和 Windows 各一条# Linux / macOS export TAOTOKEN_API_KEY你的Key # Windows PowerShell $env:TAOTOKEN_API_KEY你的Key配好之后先做一次最小连通性验证确认 Key 和地址没问题再进入 B 树调试。这一步别省否则后面节点结构对不上时你分不清是代码错还是通道错。curl -s https://taotoken.net/api/v1/models \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ | head -c 300返回里能看到模型列表就说明通道通了。如果返回鉴权错误回到 API Keys 页面确认 Key 状态如果连接超时检查 base URL 是否写成了带路径的完整地址。4. 复现叶节点分裂从插入第 15 行到内部节点指针更新现在进入正题。假设你的LEAF_NODE_MAX_CELLS是 14也就是一个叶节点最多放 14 个键值对。那么插入第 15 行时num_cells LEAF_NODE_MAX_CELLS成立触发分裂。整个流程分四步我按执行顺序拆开。第一步旧节点和新节点都拿到手。旧节点就是当前游标所在的页新节点从get_unused_page_num分配当前实现里直接返回pager-num_pages也就是文件末尾追加一页。void* old_node get_page(cursor-table-pager, cursor-page_num); uint32_t new_page_num get_unused_page_num(cursor-table-pager); void* new_node get_page(cursor-table-pager, new_page_num); initialize_leaf_node(new_node);第二步把 15 个单元14 个旧数据 1 个新数据从右往左重新分配。这里有个容易写错的细节循环从LEAF_NODE_MAX_CELLS递减到 0共 15 次i LEAF_NODE_LEFT_SPLIT_COUNT的进右节点否则进左节点。当i cursor-cell_num时写入新值i cursor-cell_num时从旧节点搬i-1位置的单元否则搬i位置的单元。for (int32_t i LEAF_NODE_MAX_CELLS; i 0; i--) { void* destination_node; if (i LEAF_NODE_LEFT_SPLIT_COUNT) { destination_node new_node; } else { destination_node old_node; } uint32_t index_within_node i % LEAF_NODE_LEFT_SPLIT_COUNT; void* destination leaf_node_cell(destination_node, index_within_node); if (i cursor-cell_num) { serialize_row(value, destination); } else if (i cursor-cell_num) { memcpy(destination, leaf_node_cell(old_node, i - 1), LEAF_NODE_CELL_SIZE); } else { memcpy(destination, leaf_node_cell(old_node, i), LEAF_NODE_CELL_SIZE); } }第三步更新两个叶节点头部的计数。左节点拿LEAF_NODE_LEFT_SPLIT_COUNT右节点拿LEAF_NODE_RIGHT_SPLIT_COUNT。当LEAF_NODE_MAX_CELLS为 14 时总数 15右节点 7 个左节点 8 个。*(leaf_node_num_cells(old_node)) LEAF_NODE_LEFT_SPLIT_COUNT; *(leaf_node_num_cells(new_node)) LEAF_NODE_RIGHT_SPLIT_COUNT;第四步处理父节点。如果旧节点是根说明它没有父节点需要创建一个新根把旧根复制成左子新节点作为右子。这一步是整节最容易出指针错误的地方。if (is_node_root(old_node)) { return create_new_root(cursor-table, new_page_num); } else { printf(Need to implement updating parent after split\n); exit(EXIT_FAILURE); }create_new_root里先把旧根整页复制到新分配的左子页清掉左子的根标记再把根页重新初始化成内部节点写入一个键和两个子指针。注意internal_node_key(root, 0)取的是左子的最大键也就是左子最后一个单元的键。void create_new_root(Table* table, uint32_t right_child_page_num) { void* root get_page(table-pager, table-root_page_num); void* right_child get_page(table-pager, right_child_page_num); uint32_t left_child_page_num get_unused_page_num(table-pager); void* left_child get_page(table-pager, left_child_page_num); memcpy(left_child, root, PAGE_SIZE); set_node_root(left_child, false); initialize_internal_node(root); set_node_root(root, true); *internal_node_num_keys(root) 1; *internal_node_child(root, 0) left_child_page_num; uint32_t left_child_max_key get_node_max_key(left_child); *internal_node_key(root, 0) left_child_max_key; *internal_node_right_child(root) right_child_page_num; }内部节点的布局要单独说清楚否则你读字节时会懵。头部依次是公共头、键数量、最右子页号主体是「子页号 键」交替的数组。内部节点永远比键多一个子指针多出来的那个就是最右子。const uint32_t INTERNAL_NODE_NUM_KEYS_SIZE sizeof(uint32_t); const uint32_t INTERNAL_NODE_NUM_KEYS_OFFSET COMMON_NODE_HEADER_SIZE; const uint32_t INTERNAL_NODE_RIGHT_CHILD_SIZE sizeof(uint32_t); const uint32_t INTERNAL_NODE_RIGHT_CHILD_OFFSET INTERNAL_NODE_NUM_KEYS_OFFSET INTERNAL_NODE_NUM_KEYS_SIZE; const uint32_t INTERNAL_NODE_HEADER_SIZE COMMON_NODE_HEADER_SIZE INTERNAL_NODE_NUM_KEYS_SIZE INTERNAL_NODE_RIGHT_CHILD_SIZE;5. 验证请求与成功结果用 .btree 确认指针指向正确代码写完不算完必须验证。把.btree命令改成递归打印才能看到多层结构。先删掉旧的print_leaf_node换成带缩进的print_tree。void indent(uint32_t level) { for (uint32_t i 0; i level; i) { printf( ); } } void print_tree(Pager* pager, uint32_t page_num, uint32_t indentation_level) { void* node get_page(pager, page_num); uint32_t num_keys, child; switch (get_node_type(node)) { case (NODE_LEAF): num_keys *leaf_node_num_cells(node); indent(indentation_level); printf(- leaf (size %d)\n, num_keys); for (uint32_t i 0; i num_keys; i) { indent(indentation_level 1); printf(- %d\n, *leaf_node_key(node, i)); } break; case (NODE_INTERNAL): num_keys *internal_node_num_keys(node); indent(indentation_level); printf(- internal (size %d)\n, num_keys); for (uint32_t i 0; i num_keys; i) { child *internal_node_child(node, i); print_tree(pager, child, indentation_level 1); indent(indentation_level 1); printf(- key %d\n, *internal_node_key(node, i)); } child *internal_node_right_child(node); print_tree(pager, child, indentation_level 1); break; } }然后跑一次插入 1 到 14、打印、再插入 15、退出的脚本。预期输出应该是根是internal (size 1)下面挂两个leaf (size 7)左叶是 1 到 7右叶是 8 到 14中间夹一行- key 7表示左子的最大键。db Tree: - internal (size 1) - leaf (size 7) - 1 - 2 - 3 - 4 - 5 - 6 - 7 - key 7 - leaf (size 7) - 8 - 9 - 10 - 11 - 12 - 13 - 14 db Need to implement searching an internal node看到- key 7这一行就说明内部节点的键取对了它等于左子的最大键。看到两个leaf (size 7)说明分裂计数正确。看到internal (size 1)说明新根只带一个键、两个子指针符合预期。最后那句Need to implement searching an internal node是正常的因为多层查找还没实现属于下一节的活。如果你想让 AI 帮你比对这份输出把上面的树结构和你的实际输出一起贴进模型对话让它逐行核对页号、键值、计数。用统一通道的好处是你不用在多个工具间复制粘贴 Key配置一次就能反复用。6. 本篇常见错排查错误一Tried to access child_num X num_keys Y。这是internal_node_child里的越界检查触发了。多半是internal_node_num_keys写成了 0 或者子指针数量对不上。检查create_new_root里是否确实写了*internal_node_num_keys(root) 1以及internal_node_child(root, 0)是否指向左子页号。错误二.btree打印出来只有一层没有 internal。说明分裂没触发或者触发了但走了else分支直接 exit。先确认LEAF_NODE_MAX_CELLS的值和你插入的行数再确认is_node_root(old_node)返回 true。如果set_node_root(root_node, true)在初始化时漏了根标记就是 false分裂会走错分支。错误三左叶和右叶的键有重叠或丢失。这是重分配循环的边界写错了。重点看i cursor-cell_num时搬的是i - 1还是i。新值插入位置之后的单元要整体右移一位所以搬i - 1插入位置之前的单元原地不动搬i。这两行写反键就会错位。错误四get_node_max_key对内部节点返回错值。内部节点的最大键是最右键也就是internal_node_key(node, num_keys - 1)叶节点是最后一个单元的键。如果内部节点分支写成了num_keys而不是num_keys - 1会读到越界内存打印出随机数。错误五新页号复用导致数据覆盖。当前get_unused_page_num直接返回pager-num_pages不做空闲页回收。如果你手动改过页分配逻辑要确保新页号没和已有页冲突。调试阶段建议先保持「只追加不回收」等分裂稳定了再优化。排查时如果拿不准把出错前后的节点 dump 贴给模型让它帮你逐字段核对。接入文档里有完整的请求格式说明配置对不上时优先查文档。7. 下一步把统一通道用在多层查找上叶节点分裂跑通之后你的 B 树第一次有了「高度」。接下来要做的就是实现内部节点的查找让insert 15不再停在Need to implement searching an internal node。那一步会用到本篇定义的internal_node_child、internal_node_key和get_node_max_key所以现在把指针关系理清楚后面会省很多事。如果你打算长期跟这个系列把编码类任务接到 Coding Plan 上会更顺入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。需要临时验证某个模型对节点结构的理解用模型对话就行。Key 管理和接入配置都在控制台和文档里遇到通道问题先看这两处再回头查代码。
返回列表