Redis/Sorted Set

排行榜为何同时需要跳表和哈希表

Redis 有序集合既要按成员查分数,也要按分数和名次遍历;较大集合通过两种索引服务这些访问方式。

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

工程问题#

单独使用哈希表无法高效输出有序区间;仅有有序结构又不便于直接按成员找到旧分数。

实际实现#

跳表按分数、成员排序,层级跨度支持排名;字典按成员定位分数,两者共享成员字符串。更新分数时必须维护两份索引。

处理流程#

  1. 按成员查字典确认是否存在。
  2. 插入或调整跳表中的有序位置。
  3. 同步字典中的分数引用。
  4. 范围查询定位起点后顺序遍历。

源码片段#

固定版本 · 原文摘录
src/t_zset.c7.2.0 · L1467–L1487
查看原文件
1467            /* Remove and re-insert when score changes. */1468            if (score != curscore) {1469                znode = zslUpdateScore(zs->zsl,curscore,ele,score);1470                /* Note that we did not removed the original element from1471                 * the hash table representing the sorted set, so we just1472                 * update the score. */1473                dictSetVal(zs->dict, de, &znode->score); /* Update score ptr. */1474                *out_flags |= ZADD_OUT_UPDATED;1475            }1476            return 1;1477        } else if (!xx) {1478            ele = sdsdup(ele);1479            znode = zslInsert(zs->zsl,score,ele);1480            serverAssert(dictAdd(zs->dict,ele,&znode->score) == DICT_OK);1481            *out_flags |= ZADD_OUT_ADDED;1482            if (newscore) *newscore = score;1483            return 1;1484        } else {1485            *out_flags |= ZADD_OUT_NOP;1486            return 1;1487        }
zsetAdd

已有成员改分后,字典保存的新值仍指向跳表节点的 score;新增成员先插入跳表,再把相同成员字符串和该 score 地址加入字典。两种索引因此共享成员存储,但必须同步维护。

只展示 OBJ_ENCODING_SKIPLIST 分支;listpack 条件和转换逻辑在前文。zslUpdateScore 内部可原位更新,并非每次改分都删除重插。

收益与取舍#

工程分析

带来的收益

  • 兼顾成员点查与有序范围查询。
  • 共享字符串减少重复存储。

付出的代价

  • 双索引及多层指针消耗额外内存。
  • 跳表对数性能是期望性质,并非最坏情况保证。

可比较的选择

listpack小集合紧凑省内存,但查找和插入需要更多线性工作。

版本与适用范围#

限 Redis 7.2.0。小集合可采用 listpack;元素数量、成员长度超过配置阈值,或不满足安全追加条件时,才转换为跳表编码。

参考来源#

源码、官方文档与原始论文
01
Redis 7.2.0 有序集合实现

src/t_zset.c · zsetTypeCreate; zsetAdd; zslInsert; zslGetRank

源码7.2.0

相关实现与比较#

4 个相关案例
QEMU·TCG 翻译块v9.2.0

QEMU TCG 用 QHT 保存 TranslationBlock 索引,帮助复用与当前客体执行状态一致的翻译结果。

附源码片段 · 缓存与复用
6 个匹配案例