完整教程:Redis 数据结构(下)ZSet, Hash

一、ZSet(有序集合)

ZSet 是 Redis 非常重要的数据结构,用于实现排行榜、延迟队列、区间查询等功能。

其底层有两种编码:

  1. ziplist(小规模)
  2. skiplist + dict(大规模)

Redis 会根据数量和元素大小自动选择最优结构。


1.1 ZSet 的底层结构:两种编码

(1)OBJ_ENCODING_ZIPLIST(小数据)

满足两个条件会使用 ziplist:

  1. 元素数量 < zset_max_ziplist_entries(默认 128)
  2. 每个 element + score 的大小 < zset_max_ziplist_value(默认 64 bytes)

ziplist 结构示意:

在这里插入图片描述

[zlbytes][zltail][zllen][element1][score1][element2][score2]...[zlend]

特点:

  • element 和 score 紧挨在一起:element 在前、score 在后
  • score 越小越靠前(按 score 升序存放)
  • ziplist 是连续内存,节省空间(但插入删除 O(n))

适用场景

  • 小型排行榜
  • 元素较少、value 较短
  • 内存敏感场景

缺点

  • 插入、删除需要整体移动(O(n))
  • 没有 dict 的 O(1) 查询能力
  • 不适合大规模

(2)OBJ_ENCODING_SKIPLIST(大数据)

当不满足 ziplist 条件时,使用:

dict + skiplist

示意图中结构如下:
在这里插入图片描述

RedisObject
    ↓
ZSET
    ├ dict(member → score)
    └ skiplist(score 排序)

① dict(哈希表)

dict 保存:

member → score

作用:

  • 通过 member 找 score(O(1))
  • 检查 member 是否存在

内部结构(你图中的 dict → dictht → dictEntry):

dictEntry(key=m1, val=10)
dictEntry(key=m2, val=20)
...

② skiplist(跳表)

跳表保存:

member、score 按 score 排序

结构示意图:

SkipList
 ├ header
 ├ tail
 ├ length
 └ level
SkipListNode
 ├ backward
 ├ level[]
 ├ ele=member
 └ score

跳表支持的操作:

  • 根据 score 查找区间
  • ZRANGE / ZRANK / ZREVRANK / ZREMRANGEBYSCORE

时间复杂度:

操作时间复杂度
插入O(logN)
删除O(logN)
按 score 查找O(logN)
区间查找O(logN + M)

1.2 ZSet 的核心设计思想(非常重要)

Redis 设计 ZSet 采用:

dict + skiplist(互补结构)

目的:

  • dict → 通过 member 获取 score(O(1))
  • skiplist → 通过 score 排序(O(logN))
  • 双结构保持同步

跳表提供排序能力,dict 提供快速检索能力。


1.3 ZSet 两种编码切换条件

条件编码
元素少、元素小ziplist
元素多或任一元素太大skiplist(dict + skiplist)

Redis 自动完成转换。


二、Hash 类型

Hash 用于存储字段值对(field-value),底层也有两种编码:

  1. ziplist(小 Hash)
  2. dict(大 Hash)

与 ZSet 类似,只是 Hash 不需要排序,因此不需要 skiplist。


在这里插入图片描述

2.1 OBJ_ENCODING_ZIPLIST(小 Hash)

默认使用 ziplist:

条件:

  1. 元素数量 < hash-max-ziplist-entries(默认 512)
  2. 任意 entry 长度 < hash-max-ziplist-value(默认 64 字节)

ziplist 在 Hash 中的存储方式:

[field1][value1][field2][value2]...[zlend]

示意图(你的第三张图):

zlbytes | zltail | zllen | "name" | "Jack" | "age" | 21 | zlend

特点:

  • field 和 value 存成紧邻两条 entry
  • 节省内存
  • 顺序读取快
  • 插入删除 O(n)

适合:

  • 小型用户资料(name, age)
  • 小结构配置数据

2.2 OBJ_ENCODING_HT(dict,大 Hash)

当 ziplist 超过阈值,会自动转换成 dict:

触发条件:

  1. 元素数量超过 hash-max-ziplist-entries
  2. 任意 field 或 value 长度超过 hash-max-ziplist-value

dict 的内部结构同 ZSet 使用的 dict 完全一样:

dict
 ├ ht[0]
 ├ ht[1]
 └ dictEntry(key=field, val=value)

示意图中的结构:

dictEntry(key="name", val="jack")
dictEntry(key="age", val=21)

特点:

  • 通过哈希表查询(O(1))
  • 插入、删除性能极高
  • 支持渐进式 rehash

适合:

  • 大型 Hash(几百、几千、几万 field)
  • 动态频繁更新的业务

三、ZSet vs Hash 编码规则总结

类型小数据编码转换规则大数据编码
ZSetziplist元素多或 entry >64Bskiplist + dict
Hashziplist元素多或 entry >64Bdict

两者最大区别:

项目ZSetHash
是否需要排序YesNo
大数据结构skiplist + dictdict
小数据结构ziplist(element+score)ziplist(field+value)

四、面试重点总结(非常重要)

1. ZSet 为什么使用 dict + skiplist?

回答要点:

  • dict 提供 O(1) 根据 member 查 score
  • skiplist 提供 O(logN) 排序
  • 互补结构、相互同步
  • 适合 ZRange、ZRank、按区间查询等复杂操作

2. ziplist 是如何存 ZSet 的?

  • element 和 score 挨在一起
  • 按 score 升序排序
  • 连续内存,节省空间

3. Hash 为什么也用 ziplist?

  • 节省内存
  • 小数据速度更快
  • field 和 value 是连续存储的两个 entry

4. ziplist 的缺点?

  • 插入 O(n)
  • 删除 O(n)
  • 可能产生连锁更新(prevlen 变大导致内存整体移动)
  • 不适合大数据量

5. 各编码在什么情况下切换?

Hash:

  • 超过 512 个 field → dict
  • 任意 entry 超过 64 字节 → dict

ZSet:

  • 超过 128 个元素 → skiplist
  • 任一元素超过 64 字节 → skiplist

五、背诵版总结

ZSet:

  • 小数据:ziplist(element + score 两个 entry)
  • 大数据:skiplist + dict
  • dict 做查找,skiplist 做排序

Hash:

  • 小数据:ziplist(field + value 两个 entry)
  • 大数据:dict
  • 转换阈值:512 个元素/64 字节 entry
posted on 2026-01-24 18:43  ljbguanli  阅读(0)  评论(0)    收藏  举报