2. Redis 核心数据结构:String、List、Set、ZSet、Hash 的使用场景与区别

Redis 提供了五种核心数据结构,每种结构在底层存储、操作复杂度与适用场景上存在显著差异。理解这些差异是设计缓存策略与应对并发压测的基础。本章从使用场景、底层实现与性能特征三个维度进行对比分析。

2.1 数据结构全景对比

数据结构 底层编码 最大元素数 读写复杂度 有序性 重复元素
String int / embstr / raw 512 MB O(1) — —
List quicklist 2^32 - 1 头尾 O(1),索引 O(n) 插入顺序 允许
Set intset / hashtable 2^32 - 1 O(1) 平均 无序 不允许
ZSet ziplist / skiplist + dict 2^32 - 1 O(log n) Score 排序 不允许
Hash ziplist / hashtable 2^32 - 1 O(1) 平均 — —
编码转换提示: Redis 会根据元素数量和大小自动切换底层编码。例如 Hash 在字段数小于 512 且每个字段值小于 64 字节时使用 ziplist,超过阈值后转为 hashtable。压测时需注意编码转换带来的性能抖动。

2.2 五种数据结构详解

2.2.1 String — 最基础的缓存单元

String 是 Redis 中最简单的数据结构,value 可以是字符串、整数或浮点数。它支持原子性的 INCR/DECR 操作,是计数器、分布式锁、Session 共享的首选。

# 缓存用户 Token
SET user:token:1001 "a3f2b8c9" EX 3600
# 原子自增 — 并发压测中模拟请求计数
INCR api:request:count
# 分布式锁
SET lock:order:12345 "thread-01" NX EX 10
压测关注点: String 的 SET/GET 均为 O(1) 操作,单节点 QPS 可达 10 万+。但在大 Value(>10 KB)场景下,网络序列化开销会成为瓶颈,建议压测时监控网络带宽与响应时延的 99 分位值。

2.2.2 List — 消息队列与时间轴

List 基于双向链表实现,支持左/右两端的高效插入与弹出。典型场景包括:异步任务队列、最新消息列表、日志收集缓冲区。

# 生产者 — 右侧推入任务
LPUSH task:queue "order:1001"
# 消费者 — 左侧阻塞弹出,实现可靠消费
BRPOP task:queue 0
# 分页获取最新 10 条消息
LRANGE news:timeline 0 9
避坑: LRANGE 对长列表的中间范围查询是 O(n) 操作。当列表长度超过 10 万时,LRANGE 会阻塞 Redis 主线程。压测中应避免对 List 执行大范围遍历,改用 ZSet 或专门的流式结构。

2.2.3 Set — 去重与集合运算

Set 基于哈希表实现,所有元素唯一。它支持交集、并集、差集等集合运算,适合标签系统、好友关系、抽奖去重等场景。

# 用户标签存储
SADD user:tags:1001 "java" "redis" "微服务"
# 交集 — 查找同时掌握 Java 和 Redis 的用户
SINTER user:tags:1001 user:tags:1002
# 随机弹出 — 抽奖场景
SPOP lottery:pool 3

2.2.4 ZSet — 排行榜与优先级队列

ZSet 在 Set 的基础上为每个元素关联一个 double 类型的 score,按 score 排序。底层使用跳跃表 + 字典的组合结构,兼顾高效的范围查询与单点访问。

# 实时排行榜 — 用户积分更新
ZADD leaderboard:game1 1500 "player_01"
ZADD leaderboard:game1 2300 "player_02"
# 获取 Top 10
ZREVRANGE leaderboard:game1 0 9 WITHSCORES
# 按分数范围查询 — 延迟任务调度
ZRANGEBYSCORE delay:queue 0 1680000000
压测建议: ZSet 的插入与删除均为 O(log n)。当 ZSet 元素超过 1000 万时,跳跃表层级深度增加,写操作时延会从微秒级上升至毫秒级。压测前应预估 ZSet 的最大容量,必要时做分片处理。

2.2.5 Hash — 对象存储与字段级操作

Hash 类似于 Java 的 HashMap,适合存储结构化对象。与 String 序列化整个对象相比,Hash 支持对单个字段的读写,节省带宽并减少并发冲突。

# 用户信息存储 — 字段级操作
HSET user:info:1001 name "张三" age 28 city "北京"
# 只获取年龄字段
HGET user:info:1001 age
# 原子自增年龄
HINCRBY user:info:1001 age 1
对比维度 String + JSON 序列化 Hash 字段存储
内存占用 整体序列化,冗余少 字段元数据额外开销
字段更新 需全量读写,并发冲突高 单字段更新,冲突低
适用场景 整体读取、不常修改 频繁修改部分字段

2.3 数据结构选型决策流程

Redis 数据结构选型决策流程 数据需要持久化缓存? 是否需要排序? 是 ZSet(排行榜) 否 是否需要去重? 是 Set(去重) 否 是否操作单个字段? 是 Hash(对象) 否 是否需要 FIFO 队列? 是 List(队列) 否 String(通用缓存) 决策原则: 优先选择操作复杂度最低且语义最匹配的结构。压测前需确认数据规模是否会触发编码转换,避免性能拐点。

2.4 并发压测中的数据结构选择要点

  • 热点 Key 集中访问: 避免将大量并发写入集中到单个 String 或 Hash 上,考虑使用本地缓存 + Redis 分片或 Hash Tag 打散。
  • 大 Key 扫描风险: List 和 ZSet 的单 Key 元素数超过 1 万时,DEL 操作会阻塞主线程。压测前应使用 MEMORY USAGE key 评估内存占用。
  • 原子性保障: 需要 CAS(Compare And Set)场景优先使用 String 的 SETNX 或 Lua 脚本,避免在 List/Hash 上实现分布式锁。
  • 批量操作优化: 压测中尽量使用 Pipeline 或 mget/mset 减少网络往返,但需注意 Pipeline 在 Redis Cluster 模式下不支持跨 Slot 批量。
压测实战建议: 在微服务并发压测前,先对每种数据结构执行单 Key 的基准测试(SET/GET 各 10 万次),记录 P99 时延和吞吐量。然后根据业务场景混合读写比例(如 8:2 或 5:5)进行场景压测,观察数据结构在不同并发线程数(16/32/64/128)下的性能拐点。