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 数据结构选型决策流程
下图展示了在缓存场景中,如何根据业务需求快速选择合适的数据结构:
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。这能显著降低缓存雪崩和一致性问题。