为什么每个开发者都要懂 Redis 数据结构
Redis 核心数据结构 不只是「缓存的五种容器」,而是决定你代码简洁度与系统性能的关键。很多团队把 Redis 当成只会 GET/SET 的旁路缓存,结果要么用 String 硬塞 JSON 导致更新要整体序列化,要么用 List 当消息队列却丢了可靠性。本文逐一拆解 String、Hash、List、Set、ZSet 这五种核心结构的底层实现、高频命令与典型场景,并给出一张选型速查表,帮你真正「用对结构」而不是「只会赋值」。
理解每种子结构适合的读写模式,比记住命令本身更重要。下面每节都会回答三个问题:它底层是什么、最顺手的命令有哪些、什么场景该用它。
一、String:最朴素也最常用的键值
String 是 Redis 的基石,底层根据值自动选择 int、embstr 或 raw 编码。它不只是存字符串,还能存整数(直接计数)、二进制(位图)。常见用法包括计数器、分布式锁、短_token 与 BitMap 统计。
# 计数器:阅读量、点赞数,原子自增
INCR article:1001:views
# 分布式锁:不存在才设置,10 秒自动过期
SET lock:order:123 "1" NX EX 10
# 缓存 JSON(注意整体序列化,适合读多写少)
SET user:1 "{"name":"suzhe","age":30}" EX 3600
# BitMap:日活统计,每一位代表一个用户是否活跃
SETBIT user:active:20261027 10001 1
BITCOUNT user:active:20261027
用 INCR 做限流计数、用 SET NX EX 做锁都是经典姿势。但要警惕「大 key」:一个几 MB 的 String 会阻塞单线程,过期删除也可能引发延迟毛刺。
二、Hash:对象字段的最佳载体
Hash 把键值对聚合在一个 key 下,非常适合表达「一个对象的多个字段」。相比把整个对象序列化成 String,Hash 支持单字段读写与原子自增,更新一个字段不必整体重传。
# 存用户对象,字段级更新
HSET user:1 name suzhe age 30 vip 1
HGET user:1 name
HGETALL user:1
HINCRBY user:1 age 1 # 年龄 +1,原子操作
# 购物车:每个商品一个 field,value 是数量
HSET cart:1001 sku_123 2
HINCRBY cart:1001 sku_123 1 # 加购数量 +1
Hash 与「String 存 JSON」的取舍很关键:要频繁改个别字段、要单独读某个字段,优先 Hash;要整体读写、且客户端自己做序列化更方便,才用 JSON String。
| 维度 | Hash(字段级) | String(JSON) |
|---|---|---|
| 更新单个字段 | 只改对应 field,省带宽 | 需整体读改写写回 |
| 读取单个字段 | HGET 直接取 | 需解析整个 JSON |
| 客户端依赖 | 服务端管结构 | 客户端管序列化 |
| 适用 | 对象、配置、购物车 | 整体读写、跨语言简单缓存 |
三、List:有序可重复的序列
List 底层在元素少时用 listpack(旧称 ziplist)紧凑存储,元素多时转为 quicklist(链表 + 紧凑节点)。它是「有序且可重复」的序列,适合做最新列表、简单队列与栈。
# 最新文章列表:左进右出,保留最近 10 条
LPUSH news 1001 1002 1003
LTRIM news 0 9
LRANGE news 0 9 # 取最新 10 条
# 简单队列:生产者左推,消费者右弹
LPUSH queue:tasks "{"job":"resize"}"
RPOP queue:tasks
# 栈:同侧进出
LPUSH stack a b c
LPOP stack
轻量队列用 LPUSH+RPOP 足够,但它不像 Kafka 那样有消费组与可靠确认。如果要求「至少一次、可重放、多消费者」,应改用 Streams(本站已有 Redis Streams 专文)。List 更适合「最新 N 条」「简单 FIFO」这类场景。
四、Set:去重与集合运算
Set 是无序且唯一的集合,底层用哈希表(小数据时用 listpack 编码)。它最擅长去重与集合运算:共同好友、兴趣标签交集、抽奖去重都天然适配。
# 给用户打标签(自动去重)
SADD user:1:tags redis golang docker
SADD user:2:tags redis python
# 共同兴趣:交集
SINTER user:1:tags user:2:tags # => redis
# 抽奖:随机抽取一名中奖者(不放回)
SPOP lottery:2026 1
# 判断某值是否在集合中(O(1))
SISMEMBER user:1:tags redis
与布隆过滤器不同,Set 是「精确去重」——结果 100% 准确,代价是内存随元素增长;布隆过滤器是概率去重,省内存但有误判。需要精确成员判定时用 Set,只需要「大概在不在」时用布隆过滤器。
五、ZSet(Sorted Set):带分数的有序集合
ZSet 是 Redis 里最强大的结构:每个成员带一个 score,底层用跳表(skiplist)+ 哈希实现,既能按分数排序,又能按成员 O(1) 取分。排行榜、延迟队列、优先级任务、范围统计都离不开它。
# 排行榜:分数即积分
ZADD leaderboard 100 suzhe 95 alice 88 bob
ZREVRANGE leaderboard 0 9 WITHSCORES # Top 10(高到低)
ZRANK leaderboard suzhe # 某人排名
# 延迟队列:score 设为执行时间戳
ZADD delay:queue 1761532800 "task:1"
# 消费者轮询:取到期任务
ZRANGEBYSCORE delay:queue 0 1761532800
# 范围统计:某时段内的点击数
ZCOUNT metrics:clicks 1000 2000
ZSet 的延迟队列思路很妙:把 score 设为「计划执行时间」,消费者用 ZRANGEBYSCORE 拉取已到期的任务即可。比 List 的「即时队列」多了「按时触发」能力,是轻量调度器的常用实现。
六、选型速查:什么时候用哪个
| 结构 | 关键特征 | 典型场景 | 避免误用 |
|---|---|---|---|
| String | 单值、可整数、可位图 | 计数、分布式锁、token、BitMap | 别把大对象塞成超大 String |
| Hash | 字段级 KV | 对象、配置、购物车 | 字段过多会变成大 key |
| List | 有序可重复 | 最新列表、简单队列/栈 | 别当可靠消息队列用 |
| Set | 无序唯一 | 去重、交集/差集、抽奖 | 超大数据集注意内存 |
| ZSet | 带分有序 | 排行榜、延迟队列、优先级 | 分数精度用整数避免浮点 |
七、内存与性能:大 key、编码与过期
选对结构只是第一步,生产里还Watch三项:大 key、编码优化与过期策略。大 key(如一个几 MB 的 Hash 或 List)会让单次命令阻塞、迁移变慢、集群数据倾斜。Redis 对小数据会自动用紧凑编码(listpack/intset),省内存也快,一旦超过阈值才转哈希表。
# 扫描全实例的大 key(生产用 --bigkeys 先做只读分析)
redis-cli --bigkeys
# 查看某个 key 的底层编码
DEBUG OBJECT user:1
# 输出含 encoding:hashtable / listpack / intset 等
# 主动过期(避免热点 key 同时失效造成缓存雪崩)
EXPIRE user:1 3600
# 错峰:给不同 key 加随机过期偏移
过期删除采用「惰性删除 + 定期抽样」:访问时才真正删,后台周期性抽查。这意味着大 key 过期时可能有一次删除阻塞,因此大 key 要拆分、热 key 要错峰过期。关于缓存模式与雪崩/穿透防护,可参考 Redis 缓存设计实战。
八、常见误区排障
| 误区 | 现象 | 修正 |
|---|---|---|
| 用 String 存大 JSON 又频繁改字段 | 每次整体读写,带宽与 CPU 飙升 | 改用 Hash 字段级更新 |
| List 当可靠消息队列 | 消费者崩溃丢消息、无消费组 | 改用 Streams 或专业 MQ |
| ZSet 分数用浮点 | 排序出现精度抖动 | 用整数(如毫秒时间戳、放大 100 倍) |
| 所有 key 同一过期时间 | 缓存雪崩,同一刻全失效 | 基础 TTL + 随机抖动 |
| 一个 Hash 存上万 field | 大 key 阻塞、迁移慢 | 按业务拆分多个 key 或加前缀分片 |
九、总结:把结构选对比命令背熟更重要
Redis 核心数据结构 的真正价值,在于「按访问模式选容器」:单值计数与锁用 String,对象字段用 Hash,最新序列用 List,去重交集用 Set,排序排名用 ZSet。记住这张选型表,你就能在缓存、排行榜、队列、计数等场景里写出既省内存又高性能的代码,而不再只会 SET/GET 一把梭。把它和 Redis 缓存设计 的缓存模式、以及容器化部署(Docker)结合起来,配合 GitHub Actions 的压测与 VS Code 里的 Redis 插件,你的 Redis 实践会稳很多。




