跳到主要内容

Redis 数据结构

问题

Redis 支持哪些数据结构?每种数据结构的底层实现是什么?不同场景下如何选择合适的数据结构?

答案

Redis 数据结构全景

Redis 支持 5 种基本数据结构 + 3 种特殊数据结构

数据结构底层编码典型应用场景
Stringint / embstr / raw (SDS)缓存、计数器、分布式锁
Hashlistpack / hashtable对象存储、用户信息
Listlistpack / quicklist消息队列、最新消息列表
Setintset / hashtable标签、共同好友、去重
Sorted Set (ZSet)listpack / skiplist + hashtable排行榜、延迟队列
StreamRax 树 + listpack消息队列(类 Kafka)
BitmapString(操作位)签到、布隆过滤器
HyperLogLog稀疏/密集编码UV 统计(基数估算)
Redis 7.0 编码变化

Redis 7.0 将小数据量时的底层编码从 ziplist 改为 listpack,解决了 ziplist 的连锁更新问题。本文基于 Redis 7.0+ 描述。

String

Redis 中最基础的数据结构,底层使用 SDS(Simple Dynamic String)

SDS vs C 字符串

特性C 字符串SDS
获取长度O(n) 遍历O(1) len 字段
缓冲区溢出可能溢出自动扩容
二进制安全否(\0 截断)是(用 len 判断结束)
内存分配每次修改都分配预分配 + 惰性释放

三种编码

编码条件说明
int值为整数且可用 long 表示直接存储整数值
embstr字符串长度 ≤ 44 字节对象头和 SDS 分配在连续内存中
raw字符串长度 > 44 字节对象头和 SDS 分别分配内存
常用命令
SET key value [EX seconds] [NX|XX]   # 设置值
GET key # 获取值
INCR key # 原子递增
INCRBY key increment # 原子增加指定值
SETNX key value # 不存在时设置(分布式锁基础)
MSET k1 v1 k2 v2 # 批量设置
MGET k1 k2 # 批量获取

应用场景:缓存(JSON 字符串)、计数器(INCR)、分布式锁(SETNX)、Session 共享。

Hash

适合存储对象,比 String 存储 JSON 更节省内存,且支持单字段修改。

编码转换

编码条件特点
listpack元素数 ≤ 128 且 值长度 ≤ 64 字节连续内存,紧凑
hashtable超过上述限制哈希表,O(1) 查找
常用命令
HSET user:1 name "张三" age 25        # 设置字段
HGET user:1 name # 获取字段
HMGET user:1 name age # 批量获取
HGETALL user:1 # 获取所有字段和值
HINCRBY user:1 age 1 # 字段递增
HDEL user:1 age # 删除字段

应用场景:用户信息、商品详情、购物车(user_id → {product_id: count})。

List

有序列表,可以从头尾两端操作。

编码

Redis 7.0 中 List 底层使用 quicklist(由多个 listpack 节点组成的双向链表)。

quicklist 兼顾了链表(快速头尾操作)和紧凑存储(listpack 减少内存碎片)的优点。

常用命令
LPUSH key v1 v2                        # 左侧入队
RPUSH key v1 v2 # 右侧入队
LPOP key # 左侧出队
RPOP key # 右侧出队
LRANGE key 0 -1 # 获取所有元素
LLEN key # 获取长度
BLPOP key timeout # 阻塞式左出队

应用场景:消息队列(LPUSH + BRPOP)、最新消息列表(LPUSH + LRANGE)、关注列表。

Set

无序集合,元素不重复,支持交集、并集、差集运算。

常用命令
SADD key v1 v2 v3                      # 添加元素
SMEMBERS key # 获取所有元素
SISMEMBER key value # 判断是否存在
SCARD key # 获取元素数量
SINTER key1 key2 # 交集
SUNION key1 key2 # 并集
SDIFF key1 key2 # 差集
SRANDMEMBER key count # 随机获取

应用场景:标签系统、共同好友(SINTER)、推荐系统(SDIFF)、抽奖(SRANDMEMBER)。

Sorted Set(ZSet)

有序集合,每个元素关联一个 score(分数),按分数排序。

底层实现

编码条件结构
listpack元素数 ≤ 128 且 值长度 ≤ 64 字节紧凑存储
skiplist + hashtable超过限制跳表支持范围查询 + 哈希表支持 O(1) 查分数

跳表(Skip List)

Level 4: HEAD ──────────────────────────────────────→ 50 ──→ NIL
Level 3: HEAD ──────────→ 20 ──────────────────────→ 50 ──→ NIL
Level 2: HEAD ──→ 10 ──→ 20 ──────────→ 40 ──────→ 50 ──→ NIL
Level 1: HEAD ──→ 10 ──→ 20 ──→ 30 ──→ 40 ──→ 45 → 50 ──→ NIL

跳表的特点:

  • 查找:O(log n),从高层开始跳跃查找
  • 插入/删除:O(log n)
  • 范围查询:O(log n + m),m 为范围内元素数
  • 实现比红黑树简单,范围操作更高效
为什么 ZSet 用跳表而不是红黑树

Redis 作者 antirez 给出的理由:

  1. 跳表实现更简单,维护和调试更容易
  2. 范围查询和按排名查询性能更好
  3. 可以通过调整层高参数来平衡时间和空间
常用命令
ZADD key score member                  # 添加元素
ZSCORE key member # 获取分数
ZRANK key member # 获取排名(0-based)
ZRANGE key 0 -1 WITHSCORES # 按分数升序获取
ZREVRANGE key 0 9 # 按分数降序取前 10
ZRANGEBYSCORE key min max # 按分数范围获取
ZINCRBY key increment member # 增加分数

应用场景:排行榜(ZINCRBY + ZREVRANGE)、延迟队列(score 为执行时间)、限流(滑动窗口)。

Stream

Redis 5.0 引入的消息队列数据结构,支持消费者组、消息确认等特性。

Stream 基本操作
XADD mystream * name "张三" action "login"  # 添加消息
XLEN mystream # 消息数量
XRANGE mystream - + # 读取所有消息

# 消费者组
XGROUP CREATE mystream mygroup $ MKSTREAM # 创建消费者组
XREADGROUP GROUP mygroup consumer1 COUNT 1 BLOCK 0 STREAMS mystream > # 消费消息
XACK mystream mygroup msg-id # 确认消息

应用场景:消息队列(轻量替代 Kafka/RabbitMQ)、事件驱动。

Bitmap 和 HyperLogLog

Bitmap —— 位图操作
SETBIT sign:user:1:202401 0 1          # 1 月 1 日签到
GETBIT sign:user:1:202401 0 # 查询是否签到
BITCOUNT sign:user:1:202401 # 统计签到天数
BITOP AND result key1 key2 # 位与运算
HyperLogLog —— 基数统计
PFADD page:uv:20240101 user1 user2     # 添加元素
PFCOUNT page:uv:20240101 # 统计基数(UV)
PFMERGE result key1 key2 # 合并
# 误差率约 0.81%,每个 key 仅占 12 KB

常见面试问题

Q1: Redis 有哪些数据结构?各自适合什么场景?

答案

数据结构核心特点典型场景
String简单键值对缓存、计数器、分布式锁
Hash字段级操作对象存储(用户信息)
List有序、可重复消息队列、最新 N 条
Set无序、不重复、集合运算标签、共同好友、去重
ZSet有序、按分数排序排行榜、延迟队列
Stream消息队列语义事件流、轻量 MQ
Bitmap位操作签到、在线状态
HyperLogLog基数估算UV 统计

Q2: ZSet 的底层为什么用跳表?

答案

ZSet 在数据量大时使用 跳表(skiplist)+ 哈希表 双重结构:

  • 跳表:支持 O(log n) 的范围查询和排名查询(ZRANGEZRANGEBYSCOREZRANK
  • 哈希表:支持 O(1) 的单元素查分数(ZSCORE

为什么不用红黑树:跳表实现更简单、范围操作更高效(找到起点后顺序遍历即可)、并发友好(可以更容易地支持并发修改)。

Q3: String 和 Hash 存储对象有什么区别?

答案

对比String 存 JSONHash
存储方式整个 JSON 字符串字段 → 值的映射
修改部分字段需要先 GET → 解析 → 修改 → SET直接 HSET key field value
内存效率JSON 有冗余(括号、引号)小对象用 listpack 更省内存
查询灵活性只能整体获取可以获取单个字段
适用场景整体读取、不常修改频繁修改部分字段

Q4: Redis 的 SDS 相比 C 字符串有什么优势?

答案

SDS(Simple Dynamic String)解决了 C 字符串的四个问题:

  1. O(1) 获取长度:SDS 结构体中有 len 字段,直接返回
  2. 防止缓冲区溢出:修改前自动检查并扩容
  3. 减少内存分配:使用空间预分配(扩容时多分配)和惰性释放(缩短时不立即回收)
  4. 二进制安全:不以 \0 判断结束,而是用 len 确定数据长度,可以存储图片、音频等二进制数据

Q5: Stream 和 List 做消息队列有什么区别?

答案

特性List(LPUSH + BRPOP)Stream
消费者组❌ 不支持✅ 支持
消息确认(ACK)❌ 不支持✅ 支持
消息持久化消费即删除消费后仍保留
消息回溯❌ 不支持✅ 支持(按 ID 范围读取)
阻塞读取✅ BRPOP✅ XREAD BLOCK

Stream 是更完善的消息队列方案,支持消费者组、消息确认、消息回溯等特性,但对于简单的生产消费场景 List 也很够用。

相关链接