<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>SkipList on JasperSao的博客</title><link>https://jasperstonnne.github.io/MyBlog/tags/skiplist/</link><description>Recent content in SkipList on JasperSao的博客</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sun, 30 Aug 2026 08:12:59 +0000</lastBuildDate><atom:link href="https://jasperstonnne.github.io/MyBlog/tags/skiplist/index.xml" rel="self" type="application/rss+xml"/><item><title>KV 进化（一）：给 91kvstore 接上一条跳表</title><link>https://jasperstonnne.github.io/MyBlog/p/kv-evolution-skiplist-engine/</link><pubDate>Fri, 28 Aug 2026 00:00:00 +0800</pubDate><guid>https://jasperstonnne.github.io/MyBlog/p/kv-evolution-skiplist-engine/</guid><description>&lt;img src="https://jasperstonnne.github.io/MyBlog/p/kv-evolution-skiplist-engine/cover.svg" alt="Featured image of post KV 进化（一）：给 91kvstore 接上一条跳表" /&gt;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;这篇不打算深挖跳表算法，而是记录一个外部数据结构如何从“能够独立运行”，一步步变成 91kvstore 中真正可用的存储引擎。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;目前这个 KV 存储引擎已经支持三种数据结构：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;结构体数组&lt;/li&gt;&#10;&lt;li&gt;红黑树&lt;/li&gt;&#10;&lt;li&gt;哈希表&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;那么问题来了：能不能再加入一种新的存储引擎，并且让它像现有的三个引擎一样，通过统一接口和网络命令工作？&lt;/p&gt;&#10;&lt;p&gt;这次选择的是——跳表。&lt;/p&gt;&#10;&lt;p&gt;最终希望实现这样的效果：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;SSET name Jasper&#10;SGET name&#10;SMOD name Sao&#10;SEXIST name&#10;SDEL name&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;表面上看，只是多写一个 &lt;code&gt;kvs_skiplist.c&lt;/code&gt;；但真正有价值的部分并不是手搓跳表，而是研究如何把一个外部数据结构完整地融合进现有项目。&lt;/p&gt;&#10;&lt;h2 id="先对跳表有个大致印象"&gt;先对跳表有个大致印象&#10;&lt;/h2&gt;&lt;p&gt;开始之前，我先看了一些简短的跳表介绍，比如这个视频：&lt;/p&gt;&#10;&lt;p&gt;&lt;a class="link" href="https://www.bilibili.com/video/BV1uCYLeiEzw/?spm_id_from=333.337.search-card.all.click&amp;amp;vd_source=6bacd16ae2a23fd992d2eb359c2d027b" target="_blank" rel="noopener"&#10; &gt;B站：跳表相关介绍&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;现在并不打算深入研究跳表的每一个概率细节，只需要先形成几个基本认知：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;跳表的底层仍然是一条有序链表；&lt;/li&gt;&#10;&lt;li&gt;它在普通链表上增加了多层索引；&lt;/li&gt;&#10;&lt;li&gt;查询时可以从高层快速跳过大量节点；&lt;/li&gt;&#10;&lt;li&gt;查找、插入、删除的平均时间复杂度是 &lt;code&gt;O(log n)&lt;/code&gt;。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;在工程中使用一种数据结构时，确实可以把它当成一个黑盒，但也不能完全不理解。至少要知道：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;它需要什么样的输入；&lt;/li&gt;&#10;&lt;li&gt;它如何管理内存；&lt;/li&gt;&#10;&lt;li&gt;它能提供哪些操作；&lt;/li&gt;&#10;&lt;li&gt;它的节点和数据由谁创建、由谁释放。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;理解到这个程度，就足够开始融合了。&lt;/p&gt;&#10;&lt;h2 id="写跳表之前我先整理了项目"&gt;写跳表之前，我先整理了项目&#10;&lt;/h2&gt;&lt;p&gt;我多少有点目录洁癖。&lt;/p&gt;&#10;&lt;p&gt;原来的 &lt;code&gt;.c&lt;/code&gt;、&lt;code&gt;.o&lt;/code&gt;、可执行文件和头文件全部堆在项目根目录，看着实在难受。因此没有立刻开始写跳表，而是先参照常见开源项目的结构，把工程重新整理了一遍：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;91kvstore/&#10;├── .gitignore&#10;├── .gitmodules&#10;├── Makefile&#10;│&#10;├── clients/&#10;│ ├── go-kvstore.go&#10;│ ├── javakvstore.java&#10;│ ├── js-kvstore.js&#10;│ ├── py-kvstore.py&#10;│ └── rust-kvstore.rs&#10;│&#10;├── include/&#10;│ ├── kvstore.h&#10;│ └── server.h&#10;│&#10;├── src/&#10;│ ├── kvstore.c&#10;│ ├── engines/&#10;│ │ ├── kvs_array.c&#10;│ │ ├── kvs_hash.c&#10;│ │ ├── kvs_rbtree.c&#10;│ │ └── kvs_skiplist.c&#10;│ │&#10;│ └── network/&#10;│ ├── ntyco.c&#10;│ ├── proactor.c&#10;│ └── reactor.c&#10;│&#10;├── tests/&#10;│ └── testcase.c&#10;│&#10;├── third_party/&#10;│ └── NtyCo/ # Git 子模块&#10;│&#10;├── build/ # 编译产物，不提交&#10;│ ├── kvstore.o&#10;│ ├── reactor.o&#10;│ ├── proactor.o&#10;│ ├── ntyco.o&#10;│ ├── kvs_array.o&#10;│ ├── kvs_hash.o&#10;│ ├── kvs_rbtree.o&#10;│ └── kvs_skiplist.o&#10;│&#10;└── bin/ # 可执行文件，不提交&#10; ├── kvstore&#10; └── testcase&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这样一来，各部分的职责就很直观：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;include/&lt;/code&gt; 放公共声明；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;src/engines/&lt;/code&gt; 放存储引擎；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;src/network/&lt;/code&gt; 放网络模型；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;tests/&lt;/code&gt; 放测试程序；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;third_party/&lt;/code&gt; 放第三方依赖；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;build/&lt;/code&gt; 和 &lt;code&gt;bin/&lt;/code&gt; 只保存编译产物。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;规整多了，right？&lt;/p&gt;&#10;&lt;h2 id="第一步把整数跳表改造成字符串-kv"&gt;第一步：把整数跳表改造成字符串 KV&#10;&lt;/h2&gt;&lt;p&gt;找到的简易跳表实现只支持：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;int key;&#10;int value;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;但 KV 存储引擎需要的是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;char *key;&#10;char *value;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因此节点最终变成：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;typedef struct kvs_skiplist_node {&#10; char *key;&#10; char *value;&#10; struct kvs_skiplist_node **forward;&#10;} kvs_skiplist_node_t;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里不只是把 &lt;code&gt;int&lt;/code&gt; 改成 &lt;code&gt;char *&lt;/code&gt; 那么简单。&lt;/p&gt;&#10;&lt;p&gt;整数可以直接比较：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;node-&amp;gt;key &amp;lt; key&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;字符串必须使用：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;strcmp(node-&amp;gt;key, key) &amp;lt; 0&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;同时，字符串不能只保存调用方传进来的地址。节点需要为 &lt;code&gt;key&lt;/code&gt; 和 &lt;code&gt;value&lt;/code&gt; 单独申请内存并复制内容，否则调用方的字符串失效后，跳表里保存的指针也会失效。&lt;/p&gt;&#10;&lt;p&gt;创建一个节点，大致需要依次完成：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;为节点结构体申请内存；&lt;/li&gt;&#10;&lt;li&gt;为 &lt;code&gt;key&lt;/code&gt; 申请内存；&lt;/li&gt;&#10;&lt;li&gt;复制 &lt;code&gt;key&lt;/code&gt;；&lt;/li&gt;&#10;&lt;li&gt;为 &lt;code&gt;value&lt;/code&gt; 申请内存；&lt;/li&gt;&#10;&lt;li&gt;复制 &lt;code&gt;value&lt;/code&gt;；&lt;/li&gt;&#10;&lt;li&gt;为每层 &lt;code&gt;forward&lt;/code&gt; 指针申请空间。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;每一步都可能失败。&lt;/p&gt;&#10;&lt;p&gt;如果中途失败，就必须把前面已经申请成功的内存释放掉。否则节点虽然没有创建成功，却会留下内存泄漏。&lt;/p&gt;&#10;&lt;p&gt;这部分看起来啰嗦，但它实际上比跳表算法本身更接近真实的 C 工程开发。&lt;/p&gt;&#10;&lt;h2 id="第二步补齐统一接口"&gt;第二步：补齐统一接口&#10;&lt;/h2&gt;&lt;p&gt;原始代码只有插入、查询和展示，但项目中的每个存储引擎都需要提供七个接口：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;kvs_skiplist_create&#10;kvs_skiplist_destory&#10;kvs_skiplist_set&#10;kvs_skiplist_get&#10;kvs_skiplist_del&#10;kvs_skiplist_mod&#10;kvs_skiplist_exist&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中比较复杂的是：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;set&lt;/code&gt;：查找插入位置，并更新多层指针；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;get&lt;/code&gt;：沿索引层逐级查找；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;del&lt;/code&gt;：不仅要释放节点，还必须先从每一层中摘除节点。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;code&gt;mod&lt;/code&gt; 和 &lt;code&gt;exist&lt;/code&gt; 相对简单，因为它们都可以复用内部查询函数：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;static kvs_skiplist_node_t *&#10;skiplist_search_node(kvs_skiplist_t *inst, const char *key);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;exist&lt;/code&gt; 只需要判断查询结果是否为 &lt;code&gt;NULL&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;mod&lt;/code&gt; 则是在找到节点后，先申请并复制新的 value；成功之后再释放旧 value。这个顺序非常重要：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;先申请新内存&#10; ↓&#10;申请成功&#10; ↓&#10;释放旧 value&#10; ↓&#10;替换指针&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果先释放旧 value，再发现新内存申请失败，原来的数据也就丢了。&lt;/p&gt;&#10;&lt;h2 id="第三步统一返回值"&gt;第三步：统一返回值&#10;&lt;/h2&gt;&lt;p&gt;接口能工作还不够，它必须遵守现有引擎的返回规则。&lt;/p&gt;&#10;&lt;p&gt;最终统一为：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;返回值&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;含义&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;操作成功，或者 key 存在&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;key 重复，或者 key 不存在&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;-1&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;参数或实例不合法&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;-2&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;内存申请失败&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;具体含义需要结合接口理解。&lt;/p&gt;&#10;&lt;p&gt;例如：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;kvs_skiplist_set(...)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;返回 &lt;code&gt;1&lt;/code&gt; 表示 key 已经存在。&lt;/p&gt;&#10;&lt;p&gt;而：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;kvs_skiplist_del(...)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;返回 &lt;code&gt;1&lt;/code&gt; 表示 key 不存在。&lt;/p&gt;&#10;&lt;p&gt;统一返回规则非常重要，因为协议层并不关心跳表内部发生了什么，它只根据返回值生成响应：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;0 → OK&#10;1 → EXIST 或 NO EXIST&#10;&amp;lt; 0 → ERROR&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="第四步别让测试变成纯苦力"&gt;第四步：别让测试变成纯苦力&#10;&lt;/h2&gt;&lt;p&gt;最开始，每修改一个函数，就在 &lt;code&gt;main()&lt;/code&gt; 中增加一堆 &lt;code&gt;if/else&lt;/code&gt;，重新编译、运行、检查输出。&lt;/p&gt;&#10;&lt;p&gt;写到后面发现，大量测试代码无非是在做：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;实际返回值是否等于预期返回值？&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;于是写了一个辅助函数：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;static void check_test(int condition, const char *testName)&#10;{&#10; if (condition) {&#10; printf(&amp;#34;[PASS] %s\n&amp;#34;, testName);&#10; } else {&#10; printf(&amp;#34;[FAIL] %s\n&amp;#34;, testName);&#10; }&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;测试就能简化为：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;result = kvs_skiplist_set(&#10; skipList,&#10; &amp;#34;Dad&amp;#34;,&#10; &amp;#34;Jasper&amp;#34;&#10;);&#10;&#10;check_test(&#10; result == 0,&#10; &amp;#34;set new key&amp;#34;&#10;);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;开发接口期间，可以先用 &lt;code&gt;#if 0&lt;/code&gt; 屏蔽 &lt;code&gt;main()&lt;/code&gt;，每完成一批接口只进行对象文件编译：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;gcc -Iinclude \&#10; -c src/engines/kvs_skiplist.c \&#10; -o /tmp/kvs_skiplist.o&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;等全部接口完成后，再临时启用 &lt;code&gt;main()&lt;/code&gt;，统一进行一次完整测试。&lt;/p&gt;&#10;&lt;p&gt;这样既能及时发现编译错误，也不用每改一个函数就维护一遍测试流程。&lt;/p&gt;&#10;&lt;h2 id="第五步从能运行走向融入项目"&gt;第五步：从“能运行”走向“融入项目”&#10;&lt;/h2&gt;&lt;p&gt;底层接口通过测试，只能说明跳表自己能工作。接下来才是真正的工程融合。&lt;/p&gt;&#10;&lt;p&gt;首先，把公共内容移动到 &lt;code&gt;include/kvstore.h&lt;/code&gt;：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;最大层数定义；&lt;/li&gt;&#10;&lt;li&gt;跳表节点类型；&lt;/li&gt;&#10;&lt;li&gt;跳表实例类型；&lt;/li&gt;&#10;&lt;li&gt;七个公开接口声明。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;而这些内部辅助函数仍然留在 &lt;code&gt;.c&lt;/code&gt; 文件中，并使用 &lt;code&gt;static&lt;/code&gt; 隐藏：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;skiplist_create_node&#10;skiplist_random_level&#10;skiplist_search_node&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这样外部模块只知道跳表“能做什么”，不需要知道它“具体怎么做”。&lt;/p&gt;&#10;&lt;p&gt;接着，把直接使用的：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;malloc&#10;free&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;替换为项目统一封装的：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;kvs_malloc&#10;kvs_free&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;再把：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;build/kvs_skiplist.o&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;加入 Makefile，让跳表参与整个项目的编译与链接。&lt;/p&gt;&#10;&lt;p&gt;Makefile 的具体语法还需要继续学习，但目前已经能看懂它最核心的关系：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;源文件&#10; ↓ 编译&#10;对象文件&#10; ↓ 链接&#10;可执行文件&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="第六步创建全局跳表实例"&gt;第六步：创建全局跳表实例&#10;&lt;/h2&gt;&lt;p&gt;服务器需要一个长期存在的跳表实例，保存所有客户端写入的数据。&lt;/p&gt;&#10;&lt;p&gt;因此在 &lt;code&gt;kvs_skiplist.c&lt;/code&gt; 中定义：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;kvs_skiplist_t global_skiplist;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在 &lt;code&gt;kvstore.c&lt;/code&gt; 中声明：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;extern kvs_skiplist_t global_skiplist;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里的 &lt;code&gt;extern&lt;/code&gt; 并不会创建第二个跳表，它只是告诉 &lt;code&gt;kvstore.c&lt;/code&gt;：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;这个变量在其他文件中已经定义，你可以直接使用它。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;然后把跳表加入存储引擎生命周期：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;kvs_skiplist_create(&amp;amp;global_skiplist);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;服务器退出时：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;kvs_skiplist_destory(&amp;amp;global_skiplist);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;至此，跳表拥有了完整的生命过程：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;定义 → 初始化 → 处理请求 → 销毁&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="第七步命令协议融合"&gt;第七步：命令协议融合&#10;&lt;/h2&gt;&lt;p&gt;最后，也是最关键的一步，是让客户端真的能够使用跳表。&lt;/p&gt;&#10;&lt;p&gt;加入五条新命令：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;SSET&#10;SGET&#10;SDEL&#10;SMOD&#10;SEXIST&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;项目并不存在一个“当前使用哪个 engine”的开关，而是通过命令前缀直接选择引擎：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;SET → array&#10;RSET → rbtree&#10;HSET → hash&#10;SSET → skiplist&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;例如客户端发送：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;SSET Dad Jasper&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;服务器的处理过程是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;解析 SSET&#10; ↓&#10;匹配 KVS_CMD_SSET&#10; ↓&#10;进入 switch 对应分支&#10; ↓&#10;调用 kvs_skiplist_set()&#10; ↓&#10;根据返回值生成 OK / EXIST / ERROR&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这一步完成后，跳表就不再是一个孤立的 &lt;code&gt;.c&lt;/code&gt; 文件，而是真正成为了 KV 存储引擎的一员。&lt;/p&gt;&#10;&lt;h2 id="最后用-testcase-做端到端测试"&gt;最后：用 testcase 做端到端测试&#10;&lt;/h2&gt;&lt;p&gt;底层 &lt;code&gt;main()&lt;/code&gt; 测试的是跳表接口，而 &lt;code&gt;tests/testcase.c&lt;/code&gt; 测试的是完整链路：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;测试客户端&#10; → TCP&#10; → 网络层&#10; → 协议解析&#10; → global_skiplist&#10; → 跳表接口&#10; → 协议响应&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;测试程序会自动发送：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;SSET&#10;SGET&#10;SEXIST&#10;SMOD&#10;SDEL&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;然后将服务器的实际响应与预期结果比较：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;相同 → PASS&#10;不同 → FAILED&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;相比手动输入命令，这种测试不仅省事，还能在后续修改代码时快速确认：跳表功能有没有被意外破坏。&lt;/p&gt;&#10;&lt;h2 id="小结"&gt;小结&#10;&lt;/h2&gt;&lt;p&gt;这次表面上是在“增加一个跳表”，实际上完整经历了一个新模块接入现有项目的过程：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;选择基础实现&#10; → 改造成字符串 KV&#10; → 补齐统一接口&#10; → 统一返回规则&#10; → 完成底层测试&#10; → 暴露公共声明&#10; → 接入内存管理&#10; → 接入 Makefile&#10; → 创建全局实例&#10; → 接入生命周期&#10; → 接入命令协议&#10; → 完成端到端测试&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;跳表算法当然重要，但这次更重要的收获是：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;一个数据结构能独立运行，和它能成为项目中真正可用的一部分，是两件完全不同的事。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;前者解决算法问题，后者解决接口、内存、构建、生命周期、协议和测试问题。&lt;/p&gt;&#10;&lt;p&gt;而后面这些，才是这次“KV 进化”真正有意思的地方。&lt;/p&gt;&#10;</description></item><item><title>KV 存储项目：Hash 接入、多语言客户端与 SkipList 扩展</title><link>https://jasperstonnne.github.io/MyBlog/p/kv-store-hash-multilingual-clients-skiplist/</link><pubDate>Wed, 26 Aug 2026 00:00:00 +0800</pubDate><guid>https://jasperstonnne.github.io/MyBlog/p/kv-store-hash-multilingual-clients-skiplist/</guid><description>&lt;img src="https://jasperstonnne.github.io/MyBlog/p/kv-store-hash-multilingual-clients-skiplist/cover.svg" alt="Featured image of post KV 存储项目：Hash 接入、多语言客户端与 SkipList 扩展" /&gt;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;阅读目标：在已有 Array、RBTree 和统一协议的基础上，重点复盘 Hash 如何真正成为一个可用的 KV 引擎；随后理解多语言客户端与服务端的边界，并按同一套接入模式继续扩展 SkipList。&lt;/p&gt;&#10;&lt;p&gt;本文不讲 Hash 数据结构算法本身，重点是 Hash 与现有 KV 项目的融合。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;h2 id="与前三篇的关系"&gt;与前三篇的关系&#10;&lt;/h2&gt;&lt;p&gt;本文承接以下三篇已有笔记：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://jasperstonnne.github.io/MyBlog/p/kv-store-network-layer/" &gt;《KV 存储项目网络层》&lt;/a&gt;已经讲过 Reactor、Proactor、NtyCo 和 &lt;code&gt;msg_handler&lt;/code&gt; 回调。&lt;/li&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://jasperstonnne.github.io/MyBlog/p/kv-store-protocol-array/" &gt;《KV 存储项目：协议层与数组存储》&lt;/a&gt;已经讲过 TCP 消息边界、token 解析、协议响应和 Array 的五种操作。&lt;/li&gt;&#10;&lt;li&gt;&lt;a class="link" href="https://jasperstonnne.github.io/MyBlog/p/kv-store-testing-multi-engine/" &gt;《KV 存储项目：客户端测试、压力测试与多引擎扩展》&lt;/a&gt;已经讲过 Makefile 基础、测试用例、压力测试、宏开关、5+2 接口以及 RBTree 的接入模式。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;因此本文不再重复网络收发、协议解析、Array/RBTree CRUD、通用测试方法和 5+2 的基础概念，只保留理解 Hash 接入所需的连接点。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="1-本阶段增加了什么"&gt;1. 本阶段增加了什么&#10;&lt;/h2&gt;&lt;p&gt;项目之前已经完成：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;统一网络入口&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 文本协议解析&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → Array 引擎&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → RBTree 引擎&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 客户端测试和压力测试&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;本阶段增加三项能力：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1. 把 Hash 接入现有 KVEngine&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. 用 Go / Java / Node.js / Python / Rust 操作同一个 KV 服务&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. 按相同模式接入 SkipList，并为范围查询做准备&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hash 部分最值得掌握的不是“冲突链表怎样写”，而是：已经有一份 Hash 代码以后，还要修改项目的哪些位置，它才能从客户端真正访问到？&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;类型与接口声明&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 编译和链接&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 全局实例&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → create 初始化&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 协议命令分发&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → set/get/del/mod/exist&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 客户端测试&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → destroy 释放&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;任何一环缺失，Hash 都没有完整融入项目。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="2-hash-在项目中的位置"&gt;2. Hash 在项目中的位置&#10;&lt;/h2&gt;&lt;p&gt;Hash 接入后，核心关系变成：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; kvs_protocol()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; kvs_filter_protocol()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ┌─────────────────┼─────────────────┐&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ │ │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Array RBTree Hash&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 普通命令前缀 R 命令前缀 H 命令前缀&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ │ │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; global_array global_rbtree global_hash&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;网络层仍然只调用 &lt;code&gt;kvs_protocol()&lt;/code&gt;，不需要为了 Hash 修改 Reactor、Proactor 或 NtyCo。&lt;/p&gt;&#10;&lt;p&gt;协议层也不需要知道桶、哈希下标和冲突链表，只负责把 Hash 命令映射到 &lt;code&gt;kvs_hash_set/get/del/mod/exist&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;这体现了已有分层设计带来的扩展性：&lt;strong&gt;新增存储引擎，主要修改核心层和构建层，不侵入网络层。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="3-第一个接入点kvstoreh"&gt;3. 第一个接入点：&lt;code&gt;kvstore.h&lt;/code&gt;&#10;&lt;/h2&gt;&lt;h3 id="31-增加编译开关"&gt;3.1 增加编译开关&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define ENABLE_HASH 1&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它应当像 &lt;code&gt;ENABLE_ARRAY&lt;/code&gt;、&lt;code&gt;ENABLE_RBTREE&lt;/code&gt; 一样，控制以下内容是否参与编译：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Hash 类型定义；&lt;/li&gt;&#10;&lt;li&gt;Hash 函数声明与实现；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;global_hash&lt;/code&gt;；&lt;/li&gt;&#10;&lt;li&gt;Hash 协议分支；&lt;/li&gt;&#10;&lt;li&gt;Hash 的初始化与销毁。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;宏开关的目的不是只隐藏一个结构体，而是完整地装上或拆下整个引擎。&lt;/p&gt;&#10;&lt;h3 id="32-hash-实例在项目中的最小理解"&gt;3.2 Hash 实例在项目中的最小理解&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;hashnode_s&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;hashnode_s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kt"&gt;hashnode_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;hashtable_s&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;hashnode_t&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;max_slots&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kt"&gt;kvs_hash_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;从 KV 项目角度只需知道：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;global_hash&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── nodes 桶数组，每个元素是一条冲突链的头指针&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── max_slots 桶数量&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── count 当前键值对数量&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;nodes&lt;/code&gt; 是二级指针，因为它先表示桶数组，再由每个桶元素指向节点：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nodes[0] → node → node → NULL&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nodes[1] → NULL&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nodes[2] → node → NULL&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;发生 Hash 冲突时，相同桶下标的节点通过 &lt;code&gt;next&lt;/code&gt; 串在一起。这里只把它作为理解资源释放和实例生命周期的前提，不展开 Hash 算法。&lt;/p&gt;&#10;&lt;h3 id="33-对外仍然使用已有的-52-形状"&gt;3.3 对外仍然使用已有的 5+2 形状&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;kvs_hash_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;kvs_hash_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;kvs_hash_destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;kvs_hash_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;kvs_hash_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;kvs_hash_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;kvs_hash_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;kvs_hash_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;kvs_hash_del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;kvs_hash_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;kvs_hash_mod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;kvs_hash_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;kvs_hash_exist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;kvs_hash_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;5+2 的设计价值和 RBTree 接入方式已在上一篇说明。这里的新增认识是：&lt;strong&gt;Hash 内部可以完全不同，但仍然服从项目已经形成的引擎契约。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="4-第二个接入点makefile"&gt;4. 第二个接入点：Makefile&#10;&lt;/h2&gt;&lt;p&gt;本阶段开始把每个 &lt;code&gt;.c&lt;/code&gt; 文件单独编译成 &lt;code&gt;.o&lt;/code&gt;。Hash 必须进入对象文件集合：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-make" data-lang="make"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;OBJS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; kvstore.o reactor.o proactor.o kvs_array.o &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ntyco.o kvs_rbtree.o kvs_hash.o&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;kvs_hash.o&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kvs_hash&lt;/span&gt;.&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="n"&gt;kvstore&lt;/span&gt;.&lt;span class="n"&gt;h&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;$(&lt;/span&gt;CC&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="k"&gt;$(&lt;/span&gt;CPPFLAGS&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="k"&gt;$(&lt;/span&gt;CFLAGS&lt;span class="k"&gt;)&lt;/span&gt; -c kvs_hash.c -o kvs_hash.o&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;kvstore&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;OBJS&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;$(&lt;/span&gt;CC&lt;span class="k"&gt;)&lt;/span&gt; -o &lt;span class="nv"&gt;$@&lt;/span&gt; &lt;span class="k"&gt;$(&lt;/span&gt;OBJS&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="k"&gt;$(&lt;/span&gt;LDFLAGS&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="k"&gt;$(&lt;/span&gt;LDLIBS&lt;span class="k"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;原笔记中的 &lt;code&gt;OBJS&lt;/code&gt; 没有 &lt;code&gt;kvs_hash.o&lt;/code&gt;。这意味着即使 &lt;code&gt;kvs_hash.c&lt;/code&gt; 已经写好，最终的 &lt;code&gt;kvstore&lt;/code&gt; 也没有编译、链接 Hash 实现。&lt;/p&gt;&#10;&lt;p&gt;建议把参数按职责拆开：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-make" data-lang="make"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; gcc&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CPPFLAGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; -I./NtyCo/core&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CFLAGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; -Wall -Wextra -g&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;LDFLAGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; -L./NtyCo&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;LDLIBS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; -luring -lntyco -lpthread -ldl&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;相对上一篇 Makefile 笔记，本阶段只需新增一个检查点：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;每增加一个 KV 引擎，都要确认它的目标文件同时进入编译规则和最终的 &lt;code&gt;OBJS&lt;/code&gt; 链接集合。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;如果还要分别生成 &lt;code&gt;kvstore&lt;/code&gt;、NtyCo 库和 &lt;code&gt;testcase&lt;/code&gt;，可设为三个明确目标：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-make" data-lang="make"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="n"&gt;ntyco&lt;/span&gt; &lt;span class="n"&gt;clean&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ntyco&lt;/span&gt; &lt;span class="n"&gt;kvstore&lt;/span&gt; &lt;span class="n"&gt;testcase&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;ntyco&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;$(&lt;/span&gt;MAKE&lt;span class="k"&gt;)&lt;/span&gt; -C NtyCo&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;testcase&lt;/code&gt; 应有自己的对象文件和链接规则，不要与服务端的 &lt;code&gt;main()&lt;/code&gt; 混在同一个目标中。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="5-第三个接入点实例生命周期"&gt;5. 第三个接入点：实例生命周期&#10;&lt;/h2&gt;&lt;h3 id="51-定义全局实例"&gt;5.1 定义全局实例&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#if ENABLE_HASH&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;kvs_hash_t&lt;/span&gt; &lt;span class="n"&gt;global_hash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#endif&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它与已有的 &lt;code&gt;global_array&lt;/code&gt;、&lt;code&gt;global_rbtree&lt;/code&gt; 并列，是 Hash 引擎在当前进程中的状态容器。&lt;/p&gt;&#10;&lt;h3 id="52-网络服务启动前创建"&gt;5.2 网络服务启动前创建&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#if ENABLE_HASH&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;kvs_hash_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;global_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="cm"&gt;/* 初始化失败，不能继续提供 Hash 命令 */&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#endif&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;必须先创建引擎，再进入网络循环。否则客户端已经能够发来 &lt;code&gt;HSET&lt;/code&gt;，协议层却会访问尚未初始化的桶数组。&lt;/p&gt;&#10;&lt;h3 id="53-网络服务停止后销毁"&gt;5.3 网络服务停止后销毁&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#if ENABLE_HASH&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;kvs_hash_destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;global_hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#endif&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;完整顺序为：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;创建 Array / RBTree / Hash&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;启动所选网络层&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;处理客户端命令&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;网络层退出&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;销毁 Hash / RBTree / Array&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果网络启动函数永不返回，还需要设计退出信号和优雅关闭流程，否则 &lt;code&gt;destroy&lt;/code&gt; 虽然写在 &lt;code&gt;main()&lt;/code&gt; 中，正常运行时却永远不会执行。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="6-第四个接入点协议命令"&gt;6. 第四个接入点：协议命令&#10;&lt;/h2&gt;&lt;p&gt;上一篇已经为 RBTree 增加 &lt;code&gt;RSET/RGET/RDEL/RMOD/REXIST&lt;/code&gt;。教学阶段为了让多个引擎同时存在，可以继续采用同一规则：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Array&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;RBTree&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Hash&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;语义&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;SET&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;RSET&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;HSET&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;新增键值对&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;RGET&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;HGET&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;查询 value&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;DEL&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;RDEL&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;HDEL&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;删除键值对&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;MOD&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;RMOD&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;HMOD&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;修改已有 value&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;EXIST&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;REXIST&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;HEXIST&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;判断 key 是否存在&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;协议层识别到 &lt;code&gt;HSET&lt;/code&gt; 后调用：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;kvs_hash_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;global_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;其余命令同理。协议层负责将引擎内部返回值翻译成项目已有响应：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;OK\r\n&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;EXIST\r\n&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NO EXIST\r\n&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;具体 value\r\n&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ERROR\r\n&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;需要保持的边界是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;协议层：参数数量、命令选择、响应文本&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Hash 层：key/value 保存、查询、修改、删除、资源管理&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;协议层不能直接访问 &lt;code&gt;global_hash.nodes&lt;/code&gt;，否则 Hash 内部实现一改，协议代码也必须跟着改，统一接口就失去了意义。&lt;/p&gt;&#10;&lt;h3 id="一条-hset-的新增路径"&gt;一条 &lt;code&gt;HSET&lt;/code&gt; 的新增路径&#10;&lt;/h3&gt;&lt;p&gt;前三篇已经讲过网络路径和 &lt;code&gt;SET&lt;/code&gt; 完整路径，此处只看 Hash 分支的差异：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HSET Teacher King&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 协议层识别 HSET&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 检查 token 数量为 3&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → kvs_hash_set(&amp;amp;global_hash, &amp;#34;Teacher&amp;#34;, &amp;#34;King&amp;#34;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 返回内部状态码&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 协议层生成 OK / EXIST / ERROR&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;从网络层到 token 切分的部分与以前完全相同，不需要为 Hash 重写。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="7-当前-hash-代码必须修正的工程问题"&gt;7. 当前 Hash 代码必须修正的工程问题&#10;&lt;/h2&gt;&lt;p&gt;这些问题的重点是内存所有权和项目稳定性，而不是 Hash 算法。&lt;/p&gt;&#10;&lt;h3 id="71-创建后必须把桶数组清零"&gt;7.1 创建后必须把桶数组清零&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;kvs_malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;hashnode_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;max_slots&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;memset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;hashnode_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;max_slots&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;kvs_malloc&lt;/code&gt; 返回的内存默认包含不确定值。不清零时，第一次读取 &lt;code&gt;hash-&amp;gt;nodes[idx]&lt;/code&gt; 就可能取得随机地址。&lt;/p&gt;&#10;&lt;h3 id="72-下标计算应使用实例容量"&gt;7.2 下标计算应使用实例容量&#10;&lt;/h3&gt;&lt;p&gt;当前各操作固定使用 &lt;code&gt;MAX_TABLE_SIZE&lt;/code&gt;：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAX_TABLE_SIZE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;应改为：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;max_slots&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;否则 &lt;code&gt;max_slots&lt;/code&gt; 字段形同虚设，将来改变容量或扩容时也会出错。&lt;/p&gt;&#10;&lt;h3 id="73-创建节点失败要逆序回滚"&gt;7.3 创建节点失败要逆序回滚&#10;&lt;/h3&gt;&lt;p&gt;指针模式下，一个节点的申请顺序是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;node → key 副本 → value 副本&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果 value 申请失败，必须释放已经成功申请的 key 和 node。当前代码释放的是本来就为 &lt;code&gt;NULL&lt;/code&gt; 的 &lt;code&gt;kvalue&lt;/code&gt;，遗漏了前两块内存。&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;kvs_hash_set()&lt;/code&gt; 也要判断 &lt;code&gt;_create_node()&lt;/code&gt; 是否返回 &lt;code&gt;NULL&lt;/code&gt;，不能直接执行 &lt;code&gt;new_node-&amp;gt;next&lt;/code&gt;。&lt;/p&gt;&#10;&lt;h3 id="74-三条释放路径必须一致"&gt;7.4 三条释放路径必须一致&#10;&lt;/h3&gt;&lt;p&gt;开启 &lt;code&gt;ENABLE_KEY_POINTER&lt;/code&gt; 后，一个节点拥有三块内存：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;node&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── key&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── value&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;以下三条路径都必须依次释放 &lt;code&gt;key&lt;/code&gt;、&lt;code&gt;value&lt;/code&gt;、&lt;code&gt;node&lt;/code&gt;：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;删除桶的头节点；&lt;/li&gt;&#10;&lt;li&gt;删除桶内的普通节点；&lt;/li&gt;&#10;&lt;li&gt;销毁整张 Hash 表。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;当前普通节点删除路径释放完整，但头节点删除和 &lt;code&gt;destroy&lt;/code&gt; 只释放了 node，会发生内存泄漏。&lt;/p&gt;&#10;&lt;h3 id="75-mod-要先申请再替换"&gt;7.5 MOD 要先申请，再替换&#10;&lt;/h3&gt;&lt;p&gt;当前代码先 &lt;code&gt;free(node-&amp;gt;value)&lt;/code&gt;，然后申请新 value。如果申请失败，旧值已经丢失，节点中还留下悬空指针。&lt;/p&gt;&#10;&lt;p&gt;安全顺序是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;申请新 value&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 复制成功&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 保存 old_value&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → node-&amp;gt;value = new_value&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → free(old_value)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样内存申请失败时，旧值仍然有效。&lt;/p&gt;&#10;&lt;h3 id="76-其他接口一致性问题"&gt;7.6 其他接口一致性问题&#10;&lt;/h3&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;kvs_hash_mod()&lt;/code&gt; 还要检查 &lt;code&gt;value != NULL&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;固定数组模式下，&lt;code&gt;strncpy&lt;/code&gt; 后必须保证字符串以 &lt;code&gt;\0&lt;/code&gt; 结束。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;kvs_hash_count()&lt;/code&gt; 已实现，但头文件中缺少声明。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;destory&lt;/code&gt; 应逐步统一为正确拼写 &lt;code&gt;destroy&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;kvs_hash_set()&lt;/code&gt; 的参数类型建议统一写成 &lt;code&gt;kvs_hash_t *&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;exist&lt;/code&gt; 的 &lt;code&gt;0=存在、1=不存在&lt;/code&gt; 必须与 Array、RBTree 和协议响应保持一致。&lt;/li&gt;&#10;&lt;li&gt;销毁后应将 &lt;code&gt;nodes=NULL&lt;/code&gt;、&lt;code&gt;max_slots=0&lt;/code&gt;、&lt;code&gt;count=0&lt;/code&gt;，降低误用风险。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="8-多语言支持本阶段新增的认识"&gt;8. 多语言支持：本阶段新增的认识&#10;&lt;/h2&gt;&lt;p&gt;上一篇已经介绍过 TCP 客户端测试，第二篇也解释过消息边界，因此这里不再重复 socket 收发和粘包、拆包原理。&lt;/p&gt;&#10;&lt;p&gt;本阶段要建立的新认识是：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;Go、Java、Node.js、Python 和 Rust 并不直接调用 C 的 Hash 函数；它们都是同一个文本协议的客户端。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;语言&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;TCP 客户端 API&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;发出的内容&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Go&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;net.Dial&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;UTF-8 协议文本&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Java&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;Socket&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;UTF-8 协议文本&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Node.js&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;net.createConnection&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;UTF-8 协议文本&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Python&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;socket.socket&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;UTF-8 协议文本&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Rust&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;TcpStream&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;UTF-8 协议文本&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;五种语言的差异只在 socket API，服务端看到的都应该是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HSET Teacher King\r\n&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HGET Teacher\r\n&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;因此“支持第三方语言”的核心不是编写五套服务端适配层，而是稳定以下契约：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;服务器地址和端口；&lt;/li&gt;&#10;&lt;li&gt;UTF-8 编码；&lt;/li&gt;&#10;&lt;li&gt;请求结束标记；&lt;/li&gt;&#10;&lt;li&gt;命令名称和参数数量；&lt;/li&gt;&#10;&lt;li&gt;响应格式；&lt;/li&gt;&#10;&lt;li&gt;连接是单请求、长连接还是支持流水线；&lt;/li&gt;&#10;&lt;li&gt;超时与异常处理方式。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;原示例中的 IP 地址有两组：&lt;code&gt;172.16.145.132&lt;/code&gt; 和 &lt;code&gt;192.168.243.131&lt;/code&gt;，实际测试时应统一为配置项。&lt;/p&gt;&#10;&lt;h3 id="用同一场景验证五种语言"&gt;用同一场景验证五种语言&#10;&lt;/h3&gt;&lt;p&gt;不要让五份示例各自测试不同命令。它们应执行同一条状态链：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HSET lang:test C → OK&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HGET lang:test → C&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HMOD lang:test common → OK&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HGET lang:test → common&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HEXIST lang:test → EXIST&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HDEL lang:test → OK&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HGET lang:test → NO EXIST&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样验证的是“协议与 Hash 引擎对所有语言行为一致”，而不只是证明五种语言都能建立 TCP 连接。&lt;/p&gt;&#10;&lt;p&gt;正式测试还应给每个客户端加入：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;send_all/write_all&lt;/code&gt; 语义，防止一次发送没有写完；&lt;/li&gt;&#10;&lt;li&gt;按 &lt;code&gt;\r\n&lt;/code&gt; 或长度字段读取完整响应；&lt;/li&gt;&#10;&lt;li&gt;连接和读取超时；&lt;/li&gt;&#10;&lt;li&gt;非 UTF-8 或异常响应处理；&lt;/li&gt;&#10;&lt;li&gt;失败时返回非零退出码，便于自动化测试发现失败。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="9-课后扩展把-skiplist-融入-kvengine"&gt;9. 课后扩展：把 SkipList 融入 KVEngine&#10;&lt;/h2&gt;&lt;p&gt;通常称为 &lt;strong&gt;Skip List（跳表）&lt;/strong&gt;，不是 SkipTable。&lt;/p&gt;&#10;&lt;p&gt;这道动手题可以直接复用 Hash、RBTree 的接入模板：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1. kvstore.h 增加 ENABLE_SKIPLIST&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. 定义 kvs_skiplist_t&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. 声明 SkipList 的 5+2 接口&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4. 新增 kvs_skiplist.c&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;5. Makefile 加入 kvs_skiplist.o&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;6. 核心层定义 global_skiplist&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;7. 启动时 create，退出时 destroy&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;8. 协议层增加 SSET/SGET/SDEL/SMOD/SEXIST&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;9. 复制已有状态链，编写 SkipList 端到端测试&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;10. 检查失败回滚、删除和销毁时的内存所有权&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这能完成“把 SkipList 当作普通 KV 引擎接入”，但还没有发挥它的有序性。&lt;/p&gt;&#10;&lt;h3 id="91-范围查询需要扩展-52"&gt;9.1 范围查询需要扩展 5+2&#10;&lt;/h3&gt;&lt;p&gt;原有 5+2 只表达单个 key 的操作，不能表达范围查询。可以增加：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;kvs_skiplist_range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;kvs_skiplist_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;start_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;end_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;kvs_pair_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;协议层增加：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SRANGE user:100 user:200 LIMIT 50&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;设计范围接口时要明确：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;起止 key 是否包含；&lt;/li&gt;&#10;&lt;li&gt;key 按字符串还是数值排序；&lt;/li&gt;&#10;&lt;li&gt;最大结果数量；&lt;/li&gt;&#10;&lt;li&gt;输出缓冲区由谁分配和释放；&lt;/li&gt;&#10;&lt;li&gt;大结果集如何分页；&lt;/li&gt;&#10;&lt;li&gt;遍历期间发生修改时怎样保证一致性。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Hash 适合精确点查，但没有顺序；SkipList 和 RBTree 保持 key 有序，更适合范围查询。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="10-面试题"&gt;10. 面试题&#10;&lt;/h2&gt;&lt;h3 id="101-kv-存储为什么常采用单线程执行命令"&gt;10.1 KV 存储为什么常采用单线程执行命令？&#10;&lt;/h3&gt;&lt;p&gt;首先修正问题前提：KV 存储不是必须使用单线程。&lt;/p&gt;&#10;&lt;p&gt;教学项目或某些内存 KV 的核心命令路径采用单线程，主要因为：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;非阻塞 I/O 可以让一个事件循环管理大量连接；&lt;/li&gt;&#10;&lt;li&gt;内存 KV 的单次操作通常很短；&lt;/li&gt;&#10;&lt;li&gt;串行执行避免共享数据上的锁竞争；&lt;/li&gt;&#10;&lt;li&gt;不需要频繁进行线程上下文切换；&lt;/li&gt;&#10;&lt;li&gt;命令顺序和数据一致性更容易理解与调试。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;它的限制也很明显：慢命令会阻塞后续请求，CPU 密集型任务无法充分利用多核，单实例吞吐最终受单核限制。&lt;/p&gt;&#10;&lt;p&gt;常见扩展方式是按 key 分片，每个分片由单线程串行执行，多个分片并行利用多核；也可以让网络 I/O、持久化、内存回收由不同线程负责。&lt;/p&gt;&#10;&lt;p&gt;结合当前项目回答时还要看所选网络模型：如果多个线程可能同时调用协议处理函数并访问同一个 &lt;code&gt;global_hash&lt;/code&gt;，Hash 就需要互斥锁、分片锁或单写者队列。不能因为包含了 &lt;code&gt;pthread.h&lt;/code&gt;，就认为数据已经线程安全。&lt;/p&gt;&#10;&lt;h3 id="102-范围查询应选择什么-kvengine"&gt;10.2 范围查询应选择什么 KVEngine？&#10;&lt;/h3&gt;&lt;p&gt;范围查询要求 key 有序：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;引擎&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;范围查询特点&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Hash&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;无序，适合点查，不适合直接做范围查询&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;SkipList&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;定位起点后沿底层链表扫描，适合本项目扩展&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;RBTree&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;中序遍历有序，也能实现范围查询&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;B/B+Tree&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;适合磁盘页和大规模范围扫描&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;LSM-Tree&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;适合高写入吞吐的持久化 KV&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;本项目的课后题应优先使用 SkipList，并在原有 5+2 之外增加 RANGE 接口。&lt;/p&gt;&#10;&lt;h3 id="103-频繁-mallocfree-时怎样管理-kv-内存"&gt;10.3 频繁 malloc/free 时怎样管理 KV 内存？&#10;&lt;/h3&gt;&lt;p&gt;当前项目已经通过 &lt;code&gt;kvs_malloc/kvs_free&lt;/code&gt; 留出了统一入口。后续可以逐步演进：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;在包装层统计分配次数、字节数、峰值和泄漏。&lt;/li&gt;&#10;&lt;li&gt;对固定大小的节点使用对象池或 slab。&lt;/li&gt;&#10;&lt;li&gt;对不同长度的 key/value 使用 size class 和空闲链表。&lt;/li&gt;&#10;&lt;li&gt;一次申请大块内存，再切成多个小对象复用。&lt;/li&gt;&#10;&lt;li&gt;多线程下使用线程本地缓存或分片内存池，降低锁竞争。&lt;/li&gt;&#10;&lt;li&gt;修改 value 时，如果旧缓冲区容量足够就复用；否则先申请新块，成功后再替换。&lt;/li&gt;&#10;&lt;li&gt;设置最大内存、淘汰策略并监控碎片率。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;内存池解决的是性能和碎片问题，不能替代正确的所有权规则。无论使用系统分配器还是内存池，都必须明确：谁申请、谁持有、谁释放，以及失败时如何回滚。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="11-本阶段的实施顺序"&gt;11. 本阶段的实施顺序&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;统一 Hash 返回值与其他引擎的语义&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 修正初始化、失败回滚、MOD 和三条释放路径&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → Makefile 加入 kvs_hash.o&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 添加 global_hash 的 create/destroy&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 协议层增加 HSET/HGET/HDEL/HMOD/HEXIST&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 复用上一篇测试状态链验证 Hash&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 五种语言运行完全相同的协议用例&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 按相同模板接入 SkipList&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 单独设计 RANGE 接口和测试&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 压测后再决定分片、加锁和内存池方案&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="总结"&gt;总结&#10;&lt;/h2&gt;&lt;p&gt;本阶段相对于前三篇真正新增的主线是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Hash 实现&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 用统一接口包装&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 进入 Makefile&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 进入全局生命周期&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 进入协议分发&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 被同一测试链验证&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 通过统一 TCP 协议服务五种语言&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;最重要的认识是：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;项目扩展的重点不是孤立地增加一种数据结构，而是把它组织成符合现有接口、生命周期、协议和测试约定的 KV 引擎。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;SkipList 的课后扩展也应遵循同一思路；只有范围查询属于新的业务能力，需要在原有 5+2 之外明确增加接口。&lt;/p&gt;&#10;</description></item></channel></rss>