news 2026/5/21 21:46:38

【网络编程】KV 存储中的 Hash 是怎么用起来的?从哈希桶到 HSET/HGET 的实现思路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【网络编程】KV 存储中的 Hash 是怎么用起来的?从哈希桶到 HSET/HGET 的实现思路

在做 KV 存储项目时,数组和红黑树都能完成键值对的增删改查,但如果目标是更快地根据 key 定位 value,那么Hash基本是绕不开的一种实现方式。

Hash 在 KV 存储中的核心价值很直接:
把“根据 key 找 value”这件事,尽量从遍历查找变成快速定位。

对于 KV 系统来说,最常见的操作其实就是:

  • SET key value
  • GET key
  • DEL key
  • MOD key value
  • EXIST key

如果底层采用哈希表来组织数据,就可以先通过 key 算出一个槽位,再去槽位里找对应节点。这样一来,KV 存储就从“顺序找”变成了“先定位、再局部查找”。而在这一套实现里,协议层也专门加上了HSET/HGET/HDEL/HMOD/HEXIST这组命令,用来把网络层收到的字符串命令分发到哈希存储逻辑中。

一、KV 存储为什么适合用 Hash

先把这个问题想清楚:
KV 存储的本质,就是维护大量的key-value映射关系。既然查找时总是先给出 key,那么最自然的思路就是想办法把 key 尽快映射到某个固定位置,这正是哈希表擅长的事情。

当前这套实现里,哈希表的整体结构可以概括成两层:

第一层是桶数组,也就是一组槽位;
第二层是链表节点,用于处理多个 key 映射到同一个槽位时的冲突问题。头文件里把节点定义成hashnode_t,表定义成hashtable_t/kvs_hash_t,其中桶数组nodes的类型是hashnode_t **,说明每个槽位存的不是一个完整对象,而是某条链表的头指针。

可以先看最关键的数据结构:

typedef struct hashnode_s { char key[MAX_KEY_LEN]; char value[MAX_VALUE_LEN]; struct hashnode_s *next; } hashnode_t; typedef struct hashtable_s { hashnode_t **nodes; // 桶数组,每个元素都是链表头指针 int max_slots; // 哈希表最大槽位数 int count; // 当前已存储元素个数 pthread_mutex_t lock; // 并发访问时的互斥锁 } hashtable_t;

这里最值得注意的是next。它说明这不是开放地址法,而是拉链法。也就是说,多个 key 如果哈希到同一个位置,不是往后探测,而是直接挂到同一个桶对应的链表上。对于 KV 存储项目来说,这种写法实现起来直观,也很适合先把逻辑跑通。

二、Hash 在 KV 中是怎么存数据的:从 key 到桶,再到节点

Hash 表真正工作的第一步,是先把 key 变成一个槽位编号。当前实现里的哈希函数比较简单:把 key 中每个字符的 ASCII 值累加起来,再对表大小取模。

static int _hash(char *key, int size) { if (!key) return -1; int sum = 0; int i = 0; while (key[i] != 0) { sum += key[i]; i++; } return sum % size; // 取模后得到桶下标 }

给这段代码加一句最通俗的解释:
同一个 key 每次都会被算到同一个桶里。

比如 key 是"Teacher",那么_hash("Teacher", 1024)会得到一个固定的下标。之后无论是HSET Teacher Charon,还是HGET Teacher,都会先算出同一个桶位置,再进入这个桶对应的链表里继续操作。

有了下标之后,插入新节点前还要先创建节点。当前实现专门写了一个_create_node(),它的作用是申请一个新节点,并把 key、value 拷贝进去:

hashnode_t *_create_node(char *key, char *value) { hashnode_t *node = (hashnode_t*)malloc(sizeof(hashnode_t)); if (!node) return NULL; strncpy(node->key, key, MAX_KEY_LEN); // 拷贝 key strncpy(node->value, value, MAX_VALUE_LEN); // 拷贝 value node->next = NULL; // 初始时不指向其他节点 return node; }

这一段其实非常像在做一个最小版的 KV 节点封装:
一个节点里保存 key、value,再通过next串成冲突链表。这样后面不管是插入、查询还是删除,操作的对象就不再是零散的字符串,而是统一的哈希节点。

三、KV 的核心操作是怎么落到 Hash 表上的

真正能体现“Hash 在 KV 存储中怎么用”的,其实就是这几个操作:插入、查询、删除、存在性判断。只要把这几个操作理顺了,整套逻辑就清楚了。

1)插入:SET/HSET

插入函数的核心思路是:

  1. 先算 key 对应的桶下标
  2. 到对应桶的链表里检查 key 是否已经存在
  3. 不存在就创建新节点
  4. 采用头插法挂到桶链表前面

代码主干如下:

int put_kv_hashtable(hashtable_t *hash, char *key, char *value) { if (!hash || !key || !value) return -1; int idx = _hash(key, MAX_TABLE_SIZE); pthread_mutex_lock(&hash->lock); // 加锁,保护并发访问 hashnode_t *node = hash->nodes[idx]; while (node != NULL) { if (strcmp(node->key, key) == 0) { pthread_mutex_unlock(&hash->lock); return 1; // 已存在 } node = node->next; } hashnode_t *new_node = _create_node(key, value); new_node->next = hash->nodes[idx]; // 头插法 hash->nodes[idx] = new_node; hash->count++; pthread_mutex_unlock(&hash->lock); return 0; }

这里最关键的有两个点。

第一个点是先查重再插入
因为 KV 存储里同一个 key 一般不能重复插入成两份数据,所以在真正创建节点前,要先遍历当前桶,看这个 key 有没有已经存在。

第二个点是头插法
new_node->next = hash->nodes[idx]; hash->nodes[idx] = new_node;的意思就是把新节点插到链表最前面。这样写实现简单,而且时间复杂度稳定,对初学阶段的 KV 项目很合适。

2)查询:GET/HGET

查询逻辑其实更直白:先算桶,再遍历桶链表,逐个比较 key。找到就返回 value,找不到就返回NULL

char *get_kv_hashtable(hashtable_t *hash, char *key) { if (!hash || !key) return NULL; int idx = _hash(key, MAX_TABLE_SIZE); pthread_mutex_lock(&hash->lock); hashnode_t *node = hash->nodes[idx]; while (node != NULL) { if (strcmp(node->key, key) == 0) { pthread_mutex_unlock(&hash->lock); return node->value; // 找到后直接返回 value } node = node->next; } pthread_mutex_unlock(&hash->lock); return NULL; // 没找到 }

这一段非常能体现 Hash 在 KV 里的定位方式:
不是全表扫描,而是先用哈希函数缩小范围,再只在一个桶里找。

这也是为什么哈希表会成为 KV 场景里的经典选择之一。因为对大量GET请求来说,查找范围被先压缩到了某个具体槽位,后面的链表遍历只是局部操作。

3)删除与存在性判断:DEL/HDELEXIST/HEXIST

删除稍微比查询复杂一点,因为它不仅要找到节点,还要把节点从链表里摘掉。当前实现把删除分成两种情况:

  • 要删除的是头节点
  • 要删除的是链表中间节点

代码主干如下:

int delete_kv_hashtable(hashtable_t *hash, char *key) { if (!hash || !key) return -2; int idx = _hash(key, MAX_TABLE_SIZE); pthread_mutex_lock(&hash->lock); hashnode_t *head = hash->nodes[idx]; if (head == NULL) return -1; // 桶为空,说明不存在 // 情况1:删除头节点 if (strcmp(head->key, key) == 0) { hashnode_t *tmp = head->next; hash->nodes[idx] = tmp; free(head); hash->count--; pthread_mutex_unlock(&hash->lock); return 0; } // 情况2:删除中间节点 hashnode_t *cur = head; while (cur->next != NULL) { if (strcmp(cur->next->key, key) == 0) break; cur = cur->next; } if (cur->next == NULL) { pthread_mutex_unlock(&hash->lock); return -1; // 没找到 } hashnode_t *tmp = cur->next; cur->next = tmp->next; free(tmp); hash->count--; pthread_mutex_unlock(&hash->lock); return 0; }

这段代码很适合作为 KV 删除逻辑的一个典型例子。
因为它不只是“找到就删”,而是必须先考虑链表链接关系,否则很容易删掉节点后把整条桶链表弄断。

而存在性判断exist则更简单,它其实是对get的复用:先查一下,只要能拿到 value,就说明 key 存在。

int exist_kv_hashtable(hashtable_t *hash, char *key) { char *value = get_kv_hashtable(hash, key); if (value) return 1; // 找到 else return 0; // 没找到 }

这里也能看出一个很典型的工程思路:
能复用查询逻辑,就不要再单独写一套“存在性查找”。

四、Hash 如何真正接入 KV 协议层

只有底层哈希表还不够,KV 存储项目里更关键的是:网络层收到的命令,最终要怎么落到哈希存储上。

当前协议层里已经把哈希相关命令加入了命令表和分发枚举中,包括:

  • HSET
  • HGET
  • HDEL
  • HMOD
  • HEXIST

服务端收到文本命令后,会先通过kvs_split_token()按空格拆分,再在kvs_filter_protocol()里识别命令字,并把请求转交给对应的哈希接口。

例如HGET的分发逻辑是:

case KVS_CMD_HGET: { char *result = kvs_hash_get(&global_hash, key); if (result == NULL) { length = sprintf(response, "NO EXIST\r\n"); } else { length = sprintf(response, "%s\r\n", result); } break; }

这段代码说明协议层真正做的事只有两步:

  1. 调用底层哈希表去查
  2. 把底层返回结果翻译成客户端能看懂的响应字符串

同样,HSETHDELHMODHEXIST也是这个思路:
协议层不直接关心桶数组、链表冲突这些细节,它只负责把“字符串命令”映射成“存储操作”。而哈希表则专心处理 key 和 value 的组织方式。

另外,在引擎初始化阶段,哈希表也被当成和数组、红黑树同级的底层存储组件统一初始化:

#if ENABLE_HASH memset(&global_hash, 0 , sizeof(kvs_hash_t)); kvs_hash_create(&global_hash); #endif

这一步的意义在于:
让哈希表真正从“一个数据结构”变成“KV 存储引擎的一部分”。
也就是说,它不再只是单独测试的容器,而是已经和协议解析、命令分发、响应返回连成了一整条链路。

总结

在 KV 存储项目里,Hash 的价值并不抽象,它解决的是一个非常具体的问题:
如何更快地根据 key 找到 value。

这套实现的整体思路可以总结成一句话:

用哈希函数先把 key 映射到桶,再用链表处理冲突,最后通过协议层把 HSET/HGET/HDEL/HMOD/HEXIST 这些命令接到哈希表操作上。

从实现角度看,最关键的点有四个:

  1. nodes维护桶数组,用hashnode_t维护桶内链表
  2. _hash()完成 key 到桶下标的映射
  3. 用链表遍历实现插入、查询、删除和存在性判断
  4. 在协议层中把文本命令映射到哈希表接口,真正接入 KV 存储流程

所以,Hash 在 KV 存储中的作用,不只是“查得快”这么简单,更重要的是它非常适合做一套围绕 key 组织数据的基础存储结构。对于一个正在逐步完善的 KV 项目来说,它往往是从“能存”走向“更适合查”的关键一步。

0voice · GitHub

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/16 4:05:59

【C#实战】WinForm窗体事件全解析与应用场景

1. WinForm窗体事件基础入门 刚接触WinForm开发时,我最困惑的就是那一大堆窗体事件到底该什么时候用。记得第一次做项目,我把所有代码都堆在Load事件里,结果界面卡得跟幻灯片似的。后来才发现,不同事件就像厨房里的各种工具——炒…

作者头像 李华
网站建设 2026/4/21 13:59:22

保姆级教程:用vLLM+Chainlit快速部署Qwen3-14B文本生成模型

保姆级教程:用vLLMChainlit快速部署Qwen3-14B文本生成模型 1. 准备工作与环境配置 1.1 了解Qwen3-14B模型 Qwen3-14B是通义千问系列中的140亿参数大语言模型,采用密集型Decoder-only Transformer架构。本教程使用的是其INT4 AWQ量化版本,通…

作者头像 李华
网站建设 2026/4/22 7:15:40

10个OpenClaw常用命令:千问3.5-9B运维指南

10个OpenClaw常用命令:千问3.5-9B运维指南 1. 环境准备与基础认知 在开始使用OpenClaw管理千问3.5-9B模型之前,我们需要先理解这套组合的技术定位。OpenClaw作为本地化AI智能体框架,与千问3.5-9B这样的开源大模型结合,可以构建出…

作者头像 李华
网站建设 2026/5/5 14:35:33

从灰度世界到边缘检测:4种AWB算法MATLAB实现对比(附完整代码)

从灰度世界到边缘检测:4种AWB算法MATLAB实现对比(附完整代码) 在工业级图像信号处理(ISP)流水线中,自动白平衡(AWB)算法是确保色彩还原准确性的关键技术。不同场景下的色温变化会导致…

作者头像 李华
网站建设 2026/4/28 17:42:43

1940-2025年各省市区县乡镇各月100m高空平均风速及风向角计算结果

各省市区县乡镇各月100m 高空平均风速及风向角计算结果1940~2025 各省市区县的平均风速_100m、平均风向角度_100m、平均风向16方位分类_100m: 1940~2025年各乡镇各月100m高空平均风速及风向角计算结果.dta 1940~2025年各城市各月100m高空平均风速及风…

作者头像 李华
网站建设 2026/4/18 8:09:03

从抽水到追击:用Python+SymPy搞定考研数学里的那些物理应用题

从抽水到追击:用PythonSymPy搞定考研数学里的那些物理应用题 考研数学中那些让人头疼的物理应用题,是否曾让你在深夜刷题时感到绝望?抽水做功要算多重积分,追击问题要解微分方程,变力做功要考虑复杂函数关系...传统的手…

作者头像 李华