在小型 Hash 中顺序查找字段
Redis 的小型 Hash 可用 listpack 紧凑保存字段和值,HGET 通过顺序扫描字段定位结果。
相关概念
工程问题#
小型 Hash 中每个字段都建立哈希节点和独立指针会增加内存成本。字段数受限时,可以用一次短扫描换取紧凑表示,但查找代价必须随规模增长受到控制。
实际实现#
HGET 经 addHashFieldToReply 调用 hashTypeGetValue。对象为 listpack 编码时,hashTypeGetFromListpack 从首元素调用 lpFind,skip=1 跳过每个字段后面的值,只比较字段;找到后用 lpNext 取值。哈希表编码走另一个分支。
处理流程#
- HGET 取得 Hash 对象,按对象编码选择查找路径
- listpack 路径从第一个字段开始比较
- 每次比较后跳过一个值元素,继续下一字段
- 匹配时读取相邻值;到末尾仍未匹配则返回未命中
源码片段#
固定版本 · 原文摘录src/t_hash.c7.2.0 · L732–L751
732 if (hashTypeGetValue(o, field, &vstr, &vlen, &vll) == C_OK) {733 if (vstr) {734 addReplyBulkCBuffer(c, vstr, vlen);735 } else {736 addReplyBulkLongLong(c, vll);737 }738 } else {739 addReplyNull(c);740 }741}742743void hgetCommand(client *c) {744 robj *o;745746 if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.null[c->resp])) == NULL ||747 checkType(c,o,OBJ_HASH)) return;748749 addHashFieldToReply(c, o, c->argv[2]->ptr);750}751addHashFieldToReply / hgetCommand
HGET 调用字段读取辅助函数;它调用 hashTypeGetValue,并分别处理字符串、整数与不存在的字段。
src/t_hash.c7.2.0 · L122–L137
122int hashTypeGetValue(robj *o, sds field, unsigned char **vstr, unsigned int *vlen, long long *vll) {123 if (o->encoding == OBJ_ENCODING_LISTPACK) {124 *vstr = NULL;125 if (hashTypeGetFromListpack(o, field, vstr, vlen, vll) == 0)126 return C_OK;127 } else if (o->encoding == OBJ_ENCODING_HT) {128 sds value;129 if ((value = hashTypeGetFromHashTable(o, field)) != NULL) {130 *vstr = (unsigned char*) value;131 *vlen = sdslen(value);132 return C_OK;133 }134 } else {135 serverPanic("Unknown hash encoding");136 }137 return C_ERR;hashTypeGetValue
按 LISTPACK 或 HT 编码分别调用顺序扫描与哈希表查找,避免将局部实现推广到所有 Hash。
src/t_hash.c7.2.0 · L79–L98
79 serverAssert(o->encoding == OBJ_ENCODING_LISTPACK);8081 zl = o->ptr;82 fptr = lpFirst(zl);83 if (fptr != NULL) {84 fptr = lpFind(zl, fptr, (unsigned char*)field, sdslen(field), 1);85 if (fptr != NULL) {86 /* Grab pointer to the value (fptr points to the field) */87 vptr = lpNext(zl, fptr);88 serverAssert(vptr != NULL);89 }90 }9192 if (vptr != NULL) {93 *vstr = lpGetValue(vptr, vlen, vll);94 return 0;95 }9697 return -1;98}hashTypeGetFromListpack
lpFind 的最后一个参数为 1:比较字段后跳过一个值;找到字段后取相邻的值。
src/listpack.c7.2.0 · L684–L704
684/* Find pointer to the entry equal to the specified entry. Skip 'skip' entries685 * between every comparison. Returns NULL when the field could not be found. */686unsigned char *lpFind(unsigned char *lp, unsigned char *p, unsigned char *s, 687 uint32_t slen, unsigned int skip) {688 int skipcnt = 0;689 unsigned char vencoding = 0;690 unsigned char *value;691 int64_t ll, vll;692 uint64_t entry_size = 123456789; /* initialized to avoid warning. */693 uint32_t lp_bytes = lpBytes(lp);694695 assert(p);696 while (p) {697 if (skipcnt == 0) {698 value = lpGetWithSize(p, &ll, NULL, &entry_size);699 if (value) {700 /* check the value doesn't reach outside the listpack before accessing it */701 assert(p >= lp + LP_HDR_SIZE && p + entry_size < lp + lp_bytes);702 if (slen == ll && memcmp(value, s, slen) == 0) {703 return p;704 }lpFind:字符串字段比较
逐条读取 listpack 元素,长度相同且字节相同时返回;完整函数另有整数编码比较。
src/listpack.c7.2.0 · L728–L749
728 /* Reset skip count */729 skipcnt = skip;730 p += entry_size;731 } else {732 /* Skip entry */733 skipcnt--;734735 /* Move to next entry, avoid use `lpNext` due to `lpAssertValidEntry` in736 * `lpNext` will call `lpBytes`, will cause performance degradation */737 p = lpSkip(p);738 }739740 /* The next call to lpGetWithSize could read at most 8 bytes past `p`741 * We use the slower validation call only when necessary. */742 if (p + 8 >= lp + lp_bytes)743 lpAssertValidEntry(lp, lp_bytes, p);744 else745 assert(p >= lp + LP_HDR_SIZE && p < lp + lp_bytes);746 if (p[0] == LP_EOF) break;747 }748749 return NULL;lpFind:跳过值并检测末尾
skipcnt 控制隔一个元素再比较,指针始终向前推进;末尾返回 NULL。
保留 HGET 到编码分派、listpack 调用方、比较与推进两个核心片段。整数比较和边界校验仍以完整上游函数为准。
收益与取舍#
工程分析带来的收益
- 小对象无需为每个字段维护独立哈希节点
- 字段和值连续存储,扫描过程直接沿 listpack 前进
付出的代价
- 最坏需要检查 n 个字段;字段字符串的比较另计字节成本
- 插入、删除和长字段会增加连续存储的移动或扫描成本
- 该策略依赖编码转换阈值,不能把 Redis Hash 的所有 HGET 路径都写成线性查找
可比较的选择
哈希表编码查找通常为期望常数次表操作,但哈希、指针和节点占用更多元数据;实际 Redis 根据对象规模及值长度选择编码。
版本与适用范围#
对应 Redis 7.2.0 的 listpack Hash 路径。配置与写入路径控制何时转换为哈希表;这里的 O(n) 指最坏字段检查次数,不等于固定耗时,也不改变 Redis 对外 Hash 接口。