Redis/Hash · listpack 编码

在小型 Hash 中顺序查找字段

Redis 的小型 Hash 可用 listpack 紧凑保存字段和值,HGET 通过顺序扫描字段定位结果。

7.2.0索引与查找核验 2026-10-12
相关概念

工程问题#

小型 Hash 中每个字段都建立哈希节点和独立指针会增加内存成本。字段数受限时,可以用一次短扫描换取紧凑表示,但查找代价必须随规模增长受到控制。

实际实现#

HGET 经 addHashFieldToReply 调用 hashTypeGetValue。对象为 listpack 编码时,hashTypeGetFromListpack 从首元素调用 lpFind,skip=1 跳过每个字段后面的值,只比较字段;找到后用 lpNext 取值。哈希表编码走另一个分支。

处理流程#

  1. HGET 取得 Hash 对象,按对象编码选择查找路径
  2. listpack 路径从第一个字段开始比较
  3. 每次比较后跳过一个值元素,继续下一字段
  4. 匹配时读取相邻值;到末尾仍未匹配则返回未命中

源码片段#

固定版本 · 原文摘录
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}751
addHashFieldToReply / 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 接口。

参考来源#

源码、官方文档与原始论文
01
addHashFieldToReply / hgetCommand

src/t_hash.c · addHashFieldToReply / hgetCommand

源码7.2.0
02
hashTypeGetValue

src/t_hash.c · hashTypeGetValue

源码7.2.0
03
hashTypeGetFromListpack

src/t_hash.c · hashTypeGetFromListpack

源码7.2.0
04
lpFind:字符串字段比较

src/listpack.c · lpFind:字符串字段比较

源码7.2.0
05
lpFind:跳过值并检测末尾

src/listpack.c · lpFind:跳过值并检测末尾

源码7.2.0
9 个匹配案例