3. Redis 核心数据结构与实战:String、Hash、List、Set、Sorted Set 在缓存场景中的选型与使用

Redis 之所以能成为缓存层的首选,不仅因为其纯内存操作的高性能,更在于它提供了五种核心数据结构,分别对应不同的业务场景。选错数据结构会导致内存浪费、性能下降甚至缓存一致性难以维护。本节将从底层编码、操作复杂度、典型缓存场景三个维度,逐一剖析五种数据结构的选型要点。

3.1 数据结构全景对比

下表从底层实现、时间复杂度、最大存储量及典型缓存用途四个角度,对五种数据结构进行横向对比:

数据结构 底层编码(3.2+) 关键操作时间复杂度 单 Key 最大容量 典型缓存场景
String int / embstr / raw GET/SET: O(1) 512 MB Session、计数器、分布式锁、对象 JSON 缓存
Hash ziplist / hashtable HGET/HSET: O(1)
HGETALL: O(N)
2^32 - 1 个 field 对象字段级缓存、用户属性、购物车
List quicklist LPUSH/RPOP: O(1)
LINDEX: O(N)
2^32 - 1 个元素 消息队列、时间线、最新动态
Set intset / hashtable SADD/SISMEMBER: O(1)
SMEMBERS: O(N)
2^32 - 1 个元素 标签系统、去重、共同好友
Sorted Set ziplist / skiplist ZADD: O(log N)
ZRANGE: O(log N + M)
2^32 - 1 个元素 排行榜、延迟队列、限流滑动窗口
选型第一原则: 如果只需要存取一个完整的序列化对象(如 JSON),优先使用 String;如果需要频繁修改对象的某个字段而不反序列化整个对象,必须使用 Hash。这是缓存一致性闭环中减少“写放大”的关键手段。

3.2 String:最通用但最容易被滥用的结构

String 是 Redis 最基础的结构,value 可以是字符串、整数或浮点数。在缓存场景中,String 通常用于存储序列化后的完整对象,但这也带来了一个隐患:任何字段的更新都需要先 GET 反序列化、修改、再 SET 整个对象,在高并发下容易产生竞态条件。

3.2.1 实战:分布式锁与计数器

// 分布式锁(SET NX + EX)
SET lock:order:1001 uuid_value NX EX 30

// 原子计数器(INCR + 过期)
INCR page_view:article:42
EXPIRE page_view:article:42 3600

// 对象缓存(JSON 序列化)
SET user:1001 '{"name":"alice","age":30,"email":"a@b.com"}' EX 300
避坑: 不要用 String 存储大文本(超过 10 KB)且需要频繁修改部分字段的数据。每次修改都要传输整个 value,浪费带宽且增加 Redis 主线程阻塞风险。此时应改用 Hash。

3.3 Hash:对象字段级缓存的正确姿势

Hash 允许对单个对象的多个字段进行独立读写,是缓存一致性闭环中“细粒度更新”的核心载体。当缓存数据与数据库字段一一对应时,Hash 可以做到只更新变更的字段,避免整对象覆盖。

3.3.1 实战:用户属性缓存

// 写入用户属性(每个字段独立)
HSET user:1001 name "alice" age 30 email "a@b.com"

// 只更新年龄字段
HSET user:1001 age 31

// 获取所有字段(慎用大对象)
HGETALL user:1001

// 只获取部分字段
HMGET user:1001 name email
缓存一致性实践: 当数据库用户表某字段更新后,只需执行 HSET user:{id} field newValue,无需删除整个缓存 Key。这避免了缓存穿透,也减少了缓存与数据库之间的不一致窗口。

3.4 List:消息队列与时间线

List 基于双向链表实现,支持在头部和尾部快速插入/弹出。在缓存场景中,List 常用于构建轻量级消息队列或存储有序的时间线数据。

3.4.1 实战:最新消息列表

// 推送新消息到头部
LPUSH timeline:user:1001 "msg_id_3001"

// 只保留最近 100 条
LTRIM timeline:user:1001 0 99

// 分页读取(LRANGE 是 O(N),N 为返回条数)
LRANGE timeline:user:1001 0 9
选型建议: 如果只需要“最新 N 条”且数据量可控(如 < 5000 条),List 是最省内存的方案。但如果需要按分数排序或去重,应改用 Sorted Set 或 Set。

3.5 Set:标签与去重

Set 提供 O(1) 的成员判断和集合运算(交集、并集、差集),非常适合缓存场景中的标签系统、权限校验和去重操作。

3.5.1 实战:用户标签与权限

// 为用户打标签
SADD user:1001:tags "vip" "developer" "beta"

// 判断是否具有某个标签
SISMEMBER user:1001:tags "vip"

// 计算两个用户的共同标签
SINTER user:1001:tags user:1002:tags

// 获取所有标签(SMEMBERS 会返回全部,大集合慎用)
SMEMBERS user:1001:tags
避坑: SMEMBERS 会返回集合中所有元素,如果集合超过数千个,会阻塞 Redis 主线程。对于大集合,应使用 SSCAN 进行游标迭代。

3.6 Sorted Set:排行榜与延迟队列

Sorted Set 为每个元素关联一个 double 类型的分数,并按照分数排序。它是实现排行榜、延迟队列和滑动窗口限流的最佳数据结构。

3.6.1 实战:实时排行榜

// 更新玩家分数
ZADD leaderboard:game:1001 1500 "player_42"
ZADD leaderboard:game:1001 2300 "player_17"

// 获取 Top 10(按分数降序)
ZREVRANGE leaderboard:game:1001 0 9 WITHSCORES

// 获取某玩家排名
ZREVRANK leaderboard:game:1001 "player_42"

// 获取分数区间内的玩家
ZRANGEBYSCORE leaderboard:game:1001 1000 2000

3.6.2 实战:延迟队列

// 将任务加入延迟队列,score 为执行时间戳
ZADD delay:queue 1700000000 "task_id_001"
ZADD delay:queue 1700000100 "task_id_002"

// 轮询获取到期的任务(当前时间戳作为分数上限)
ZRANGEBYSCORE delay:queue 0 1700000005

// 移除已获取的任务
ZREMRANGEBYSCORE delay:queue 0 1700000005
性能关键: Sorted Set 的 ZADD 和 ZRANGE 操作时间复杂度为 O(log N),在百万级元素下依然能保持毫秒级响应。但 ZRANGEBYSCORE 返回大量元素时,应配合 LIMIT 参数控制返回数量。

3.7 数据结构选型决策流程

下图展示了在缓存场景中,如何根据业务需求快速选择合适的数据结构:

Redis 缓存数据结构选型决策树 缓存数据需求 单值 / 计数器 / 锁 String 多字段 / 集合 / 排序 需要排序? 需要去重? 对象字段独立? Sorted Set Set Hash 选型说明: 1. 如果只需要存取一个完整对象且不频繁修改字段 → String 2. 如果需要频繁修改对象的部分字段 → Hash 3. 如果需要按分数排序(排行榜/延迟队列)→ Sorted Set 4. 如果只需要去重/集合运算 → Set;如果需要消息队列/时间线 → List

3.8 内存优化与编码转换

Redis 3.2+ 会根据数据量自动选择底层编码以节省内存。理解这些编码转换阈值,有助于在缓存设计阶段预估内存消耗:

数据结构 小数据编码 大数据编码 转换阈值(默认)
Hash ziplist hashtable field 数 > 512 或单个 field value > 64 字节
List quicklist(压缩节点) quicklist(非压缩) list-compress-depth 控制压缩层级
Set intset hashtable 元素数 > 512 或包含非整数元素
Sorted Set ziplist skiplist 元素数 > 128 或单个元素 value > 64 字节
优化建议: 对于 Hash 和 Sorted Set,如果确认数据量不会超过转换阈值,可以手动修改 hash-max-ziplist-entries 和 zset-max-ziplist-entries 配置,让数据始终使用 ziplist 编码,内存占用可降低 60% 以上。

3.9 缓存场景综合选型示例

以一个典型的社交 App 缓存设计为例,展示五种数据结构的协同使用:

// 1. 用户 Session(String + 过期)
SET session:token_abc '{"uid":1001,"role":"vip"}' EX 7200

// 2. 用户资料(Hash,支持字段级更新)
HSET user:1001 name "Alice" avatar "url_a" level 5

// 3. 用户动态时间线(List,保留最新 200 条)
LPUSH timeline:1001 "post_3001"
LTRIM timeline:1001 0 199

// 4. 用户标签(Set,用于推荐系统)
SADD user:1001:tags "tech" "music" "photography"

// 5. 全站热门帖子(Sorted Set,按热度排序)
ZINCRBY hot:posts 10 "post_3001"
ZREVRANGE hot:posts 0 9 WITHSCORES
一致性闭环要点: 上述五种结构在缓存更新时,应遵循“最小更新单元”原则——只更新变更的数据结构,而不是删除整个用户缓存。例如用户等级变更时,只需 HSET user:1001 level 6,而不需要删除 user:1001 整个 Key。这能显著降低缓存雪崩和一致性问题。