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 数据结构选型决策流程
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)下的性能拐点。