Redis 最容易被误解成“更快的数据库”。这个理解只对了一小半。Redis 更适合放在系统的热路径上,处理短生命周期状态、派生索引、原子协调、近实时统计和少量高频列表;完整事实仍然应该由数据库、日志或对象存储承载。

系统设计面试里,Redis 的价值也不在命令背诵。更重要的是把业务需求翻译成几个稳定的问题模型:

  • 这份数据是否可以过期
  • 这份数据丢了能否重建
  • 读路径是否远热于写路径
  • 是否需要排序、范围查询、集合运算或原子判断
  • Redis 故障时,系统还能不能保持核心正确性

答案如果把 Redis 当成事实库,通常会在持久性、审核追溯、深页查询或跨 key 一致性上掉坑。更可靠的边界是:数据库保存事实,Redis 保存热路径和派生状态。

Redis 的系统设计位置

flowchart TD
    Req["业务需求"] --> Sem["抽象操作语义"]
    Sem --> DS["选择 Redis 数据结构"]
    DS --> Key["设计 Key 和分片边界"]
    Key --> Cons["定义一致性边界"]
    Cons --> Fail["故障与降级"]

    Sem --> S1["单值状态"]
    Sem --> S2["结构化对象"]
    Sem --> S3["排序列表"]
    Sem --> S4["集合关系"]
    Sem --> S5["概率统计"]
    Sem --> S6["消息流"]

    S1 --> R1["String"]
    S2 --> R2["Hash"]
    S3 --> R3["Sorted Set"]
    S4 --> R4["Set / Bitmap"]
    S5 --> R5["HyperLogLog / Bloom"]
    S6 --> R6["Stream / List"]

Redis 方案设计可以按四步落地:

步骤 要回答的问题 常见错误
抽象操作 业务本质是读写单值、排序、集合、位图、消息流,还是原子判断 上来先套命令
设计 Key Key 的粒度、生命周期、hash slot、热点边界是什么 一个业务对象塞进一个大 key
明确事实源 哪些数据必须落数据库,哪些只是 Redis 派生状态 把 Redis 当不可丢事实库
设计降级 Redis 慢、丢、不可用时,读写路径如何退回 没有回源与修复路径

数据结构速查

数据结构 本质 适合的问题 典型命令
String 二进制安全字节串 缓存、计数、锁、Session、幂等 token GETSETINCRSET NX EX
Hash 一个 key 下的字段表 对象缓存、购物车、计数聚合 HSETHGETHINCRBY
List 双端队列 简单任务队列、最新日志 LPUSHRPOPBRPOP
Set 无序唯一集合 去重、关注关系、交并差 SADDSISMEMBERSINTER
Sorted Set 带 score 的唯一集合 排行榜、时间线、延迟队列、限流窗口 ZADDZRANGEZRANGEBYSCORE
Bitmap 位数组 签到、在线状态、大规模布尔状态 SETBITGETBITBITCOUNT
HyperLogLog 概率基数统计 UV、独立访客、粗粒度去重计数 PFADDPFCOUNT
GEO 经纬度索引,底层是 ZSet 附近的人、门店搜索 GEOADDGEOSEARCH
Stream 追加式消息日志 可靠消息队列、事件流、消费者组 XADDXREADGROUPXACK

八个可迁移模式

模式 核心公式 覆盖场景 边界
KV + TTL SET {key} {value} EX {ttl} 缓存、Session、验证码、短链接 适合可过期状态
原子占位 SET {key} {owner} NX EX {ttl} 分布式锁、幂等、击穿保护 不能替代数据库事务
Score 即数轴 ZADD {key} {score} {member} 排行榜、延迟队列、时间线、限流 大 ZSet 和热 key 要治理
双向关系 SADD A BSADD B A 关注、粉丝、共同好友、权限交集 大 V 关系不适合全量 Set
实体 + 索引分离 HSET entity + ZADD index 评论、商品列表、搜索结果缓存 Redis 索引是派生状态
概率换空间 PFADD / BF.ADD UV、穿透防护、大规模去重 允许误差或误判
位图压缩 SETBIT {key} {offset} 1 签到、在线、开关状态 offset 需要可控
Lua 原子胶水 多命令封进脚本 限流、锁释放、延迟队列消费 Cluster 下跨 slot 受限

用例一:缓存系统

问题

数据库中的热点数据被反复读取,查询成本高,响应慢。缓存系统要降低数据库压力,同时避免脏读、击穿、穿透和雪崩。

设计

最常见的模式是 Cache Aside。应用先读 Redis,未命中再读数据库,并把结果写回 Redis。

sequenceDiagram
    participant App
    participant Redis
    participant DB

    App->>Redis: GET cache:user:123
    alt 命中
        Redis-->>App: 用户缓存
    else 未命中
        Redis-->>App: nil
        App->>DB: SELECT user
        DB-->>App: 用户事实
        App->>Redis: SET cache:user:123 {json} EX 3600
    end
1
2
3
GET cache:user:123
SET cache:user:123 "{json}" EX 3600
DEL cache:user:123

写路径通常采用“先更新数据库,再删除缓存”。删除比更新缓存更稳,因为缓存值可能由多张表、多段业务逻辑组合而来,写路径未必拿得到完整新值。

三类缓存问题

问题 现象 处理
缓存穿透 查询不存在的数据,每次都打到数据库 空值缓存短 TTL,或 Bloom Filter 预判
缓存击穿 热点 key 过期,大量请求同时回源 单飞互斥、逻辑过期、旧值兜底
缓存雪崩 大量 key 同时过期或 Redis 故障 TTL 加随机偏移,多级缓存,限流降级
1
2
SET cache:null:user:404 "" EX 60
SET lock:rebuild:user:123 {requestId} NX EX 10

取舍

Redis 缓存适合动态热点数据,不适合替代数据库保存核心事实。缓存一致性要控制的是脏数据窗口:业务能接受多长时间的不一致,就按这个窗口设计主动失效、被动回源和后台修复路径。

模式提炼:KV + TTL

任何“有生命周期、可重建、读多写少”的状态,都可以先按 KV + TTL 建模。

场景 Key Value TTL
用户缓存 cache:user:{id} 用户 JSON 10 分钟
商品详情 cache:product:{id} 商品 JSON 5 分钟
空值缓存 cache:null:user:{id} 空字符串 30 秒
短链解析 short:{code} 原始 URL 7 天

用例二:Session、验证码与临时令牌

问题

多实例服务需要共享登录态、验证码、临时授权码和幂等 token。这些状态有天然过期时间,访问频繁,数据量可控。

设计

1
2
3
4
5
6
7
SET session:{token} "{userId, deviceId, roles}" EX 1800
GET session:{token}
EXPIRE session:{token} 1800
DEL session:{token}

SET sms:code:{phone} "384921" EX 300
SET idem:order:{userId}:{clientToken} "{orderId}" NX EX 86400

Session 的事实源通常来自用户登录事件和账户系统。Redis 保存的是“当前有效状态”。Redis 故障时,可以让用户重新登录;这影响体验,但不破坏核心账务事实。

取舍

方案 优势 代价
本地 Session 延迟最低 多实例扩展差
Redis Session 多实例共享,过期简单 Redis 可用性影响登录态
JWT 服务端无状态 撤销、续期、权限变更更复杂

模式提炼:短生命周期状态

验证码、登录态、授权码、幂等 token 的共同点是“状态本身会自然死亡”。TTL 在这里不是附加配置,它就是数据模型的一部分。

用例三:分布式锁与幂等控制

问题

多个服务实例同时处理同一资源时,需要保证某段临界区同一时间只有一个执行者。典型场景包括缓存重建、定时任务抢占、重复请求防重。

设计

加锁使用 SET NX EX,释放锁必须校验 owner 后删除。

1
SET lock:order:123 {uuid} NX EX 30

释放锁用 Lua:

1
2
3
4
5
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end

value 不能省略。没有 owner 校验时,一个慢请求可能删掉另一个请求刚拿到的锁。

Redlock 与看门狗

Redlock 试图在多个独立 Redis 节点上获取多数派锁,降低单点故障风险。它适合“重复执行代价可控”的互斥场景,不适合金融账务、库存最终扣减这类强一致临界区。强一致需求应该回到数据库事务、乐观锁、唯一约束或专门的协调系统。

看门狗续期适合执行时间不确定的任务,但会引入新的风险:业务线程卡死时,锁可能被错误续期。续期机制必须绑定持有者健康状态,并设置最大持有时间。

取舍

适合 Redis 锁 不适合 Redis 锁
缓存重建互斥 资金扣减
定时任务抢占 唯一订单创建
幂等处理中间状态 强一致库存扣减
重复请求短期防重 长事务资源锁定

模式提炼:原子占位

SET NX 表达的是“谁先到谁占坑”。分布式锁只是其中一种用法。

场景 Key Value TTL
缓存击穿保护 lock:rebuild:{key} 请求 ID 10 秒
接口幂等 idem:pay:{token} 处理结果 24 小时
任务抢占 job:owner:{jobId} worker ID 60 秒

用例四:计数器与限流器

问题

阅读量、点赞数、库存预扣、接口访问频率都需要高并发更新。数据库每次加一会产生热点行,应用内计数又无法跨实例共享。

设计:计数器

1
2
3
INCR view:post:123
HINCRBY cnt:post:123 like 1
HINCRBY cnt:post:123 reply 1

计数分两类。余额、库存、账务属于事实,不能只放 Redis。阅读量、点赞展示数、评论展示数通常是派生指标,可以 Redis 聚合后异步落库,并由后台定期校准。

设计:滑动窗口限流

1
2
3
4
ZREMRANGEBYSCORE rl:api:{userId} 0 {nowMinusWindow}
ZCARD rl:api:{userId}
ZADD rl:api:{userId} {nowMillis} {requestId}
EXPIRE rl:api:{userId} 120

这组命令要封进 Lua,避免并发请求在 ZCARDZADD 之间穿过限制。

设计:令牌桶

令牌桶适合“允许瞬时突发,但限制长期平均速率”的接口。Redis Hash 可以保存当前令牌数和上次补充时间。

1
2
key = tb:{userId}:{api}
fields = tokens, last_refill_millis

令牌桶的计算必须在 Lua 中完成:读状态、补令牌、判断、扣令牌、写回状态是一组原子动作。

取舍

方案 精度 成本 适合场景
固定窗口 粗限流
滑动窗口 登录、评论、发帖
令牌桶 API 网关、突发流量

模式提炼:Score 即时间轴

滑动窗口的本质是把请求时间放进 ZSet 的 score。凡是“按时间范围取一段数据”的问题,都可以先尝试 ZSet。

用例五:排行榜

问题

排行榜需要实时更新分数、查询 Top N、查询某个用户名次、查询用户附近的排名。数据库可以做排序,但高频更新和实时 Top N 会很昂贵。

设计

1
2
3
4
5
ZADD rank:game:daily 1800 player:1
ZINCRBY rank:game:daily 20 player:1
ZREVRANGE rank:game:daily 0 9 WITHSCORES
ZREVRANK rank:game:daily player:1
ZSCORE rank:game:daily player:1

多周期榜单通常按时间拆 key:

1
2
3
ZADD rank:game:2025-07-28 1800 player:1
EXPIRE rank:game:2025-07-28 172800
ZUNIONSTORE rank:game:week:2025-W31 7 rank:game:2025-07-28 ...

设计要点

问题 处理
同分排序 score 编码进次级排序,或应用层按 ID 二次排序
榜单过大 只保留 Top N,长尾落数据库
多维排序 Redis 不适合复杂组合排序,交给搜索引擎或 OLAP
作弊分数回滚 分数事件化,保留可重算依据

模式提炼:分数数轴

ZSet 的 score 不一定是“分数”,也可以是时间戳、过期时间、热度分、距离编码。业务可以映射到一条数轴时,就能使用 ZSet 做范围和排名。

用例六:延迟队列

问题

订单超时关闭、30 分钟后发送提醒、延迟重试等需求都需要“到时间再执行”。专业 MQ 往往有延迟消息能力;没有 MQ 或规模较小时,Redis ZSet 可以实现轻量延迟队列。

设计

1
2
3
ZADD delay:order:close {executeAtMillis} order:123
ZRANGEBYSCORE delay:order:close 0 {nowMillis} LIMIT 0 10
ZREM delay:order:close order:123

查询到期消息和删除消息必须原子化,否则多个消费者会重复处理同一条消息。

1
2
3
4
5
local items = redis.call("ZRANGEBYSCORE", KEYS[1], 0, ARGV[1], "LIMIT", 0, ARGV[2])
for _, item in ipairs(items) do
redis.call("ZREM", KEYS[1], item)
end
return items

取舍

Redis 延迟队列适合低到中等规模的延迟任务,不适合长时间海量堆积、严格投递保证、复杂重试策略。任务结果必须幂等,消费者失败后要能补偿或重入。

用例七:社交关系

问题

关注、粉丝、共同好友、是否互相关注等需求,本质上是集合成员关系和集合运算。

设计

1
2
3
4
5
6
7
8
9
SADD following:userA userB
SADD followers:userB userA

SREM following:userA userB
SREM followers:userB userA

SISMEMBER following:userA userB
SINTER following:userA following:userB
SDIFF following:userA following:userB

互相关注可以在关注写入时顺手判断:

1
SISMEMBER following:userB userA

若成立,再写入 friends:userAfriends:userB

取舍

普通用户的关注列表适合 Set。大 V 粉丝列表可能达到千万级,完整放进单个 Set 会带来大 key、迁移慢、集合运算阻塞等问题。大 V 粉丝可以只存计数,或按分片 key 存储,推荐列表交给专门的图计算或推荐系统。

模式提炼:双向关系

一个关系通常有两个查询方向。关注关系要查“用户 A 关注了谁”,也要查“谁关注了用户 A”。写两份 Set 是有意的冗余,用写放大换读简单。

用例八:SNS 二级评论系统

问题

评论系统表面是 CRUD,实际考察产品语义、分页稳定性、热点治理、审核删除和缓存边界。一个 SNS 评论系统通常要支持:

维度 设计假设
层级 帖子下有一级评论,一级评论下有回复
排序 默认按时间倒序,可扩展热度排序
展示 首屏展示一级评论,每条带少量回复预览
删除 作者删除、平台隐藏、审核中状态
一致性 评论事实不能丢,计数和排序可以短暂延迟
风控 限流、敏感词、重复提交、恶意刷屏

假设 1 亿 DAU,10% 用户每天发表评论,活跃评论用户日均 2 条:

指标 粗估
写入量 100M * 10% * 2 = 20M/day
平均写 QPS 230/s
峰值写 QPS 2K/s
读写比 20:1
峰值读 QPS 热点帖子可到 50K/s+

架构

flowchart TD
    Client["客户端"] --> API["评论服务"]
    API --> Guard["幂等与限流"]
    Guard --> Redis[("Redis 热路径")]
    API --> DB[("评论数据库")]
    API --> Outbox[("Outbox 事件表")]
    Outbox --> Worker["异步 Worker"]
    Worker --> Redis
    Worker --> Audit["审核服务"]
    Worker --> Notify["通知服务"]
    Worker --> Search["搜索索引"]

数据库保存事实,Redis 保存热路径。Outbox 让数据库提交后的事件可靠进入缓存更新、通知、审核和搜索索引链路。

数据模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CREATE TABLE comments (
comment_id BIGINT PRIMARY KEY,
post_id BIGINT NOT NULL,
root_comment_id BIGINT NOT NULL,
parent_id BIGINT NOT NULL DEFAULT 0,
author_id BIGINT NOT NULL,
reply_to_user_id BIGINT,
content TEXT NOT NULL,
status TINYINT NOT NULL,
like_count BIGINT NOT NULL DEFAULT 0,
reply_count BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
version BIGINT NOT NULL DEFAULT 0
);

CREATE INDEX idx_post_parent_time
ON comments(post_id, parent_id, created_at DESC, comment_id DESC);

CREATE INDEX idx_root_time
ON comments(root_comment_id, created_at ASC, comment_id ASC);

一级评论满足 parent_id = 0root_comment_id = comment_id。回复评论的 root_comment_id 指向一级评论,parent_id 指向直接回复对象。这样既能表达“回复某人”,又避免读取时递归展开无限树。

Redis Key

用途 Key 类型
帖子一级评论热段 cmt:post:{postId}:top:new ZSet
一级评论回复热段 cmt:root:{rootCommentId}:reply:new ZSet
评论体短缓存 cmt:body:{commentId} String / Hash
帖子评论计数 cmt:cnt:post:{postId} String / Hash
回复计数 cmt:cnt:root:{rootCommentId} String / Hash
用户限流 rl:cmt:{userId} ZSet
幂等 token idem:cmt:{userId}:{token} String
1
2
3
ZADD cmt:post:{postId}:top:new {createdAtMillis} {commentId}
ZREMRANGEBYRANK cmt:post:{postId}:top:new 0 -1001
EXPIRE cmt:post:{postId}:top:new 3600

Redis 只缓存前 500 或 1000 条热评论 ID。深页走数据库索引。缓存全量评论只会把数据库压力搬成 Redis 大 key。

写路径

  1. 鉴权和权限检查。
  2. Redis 用户维度限流。
  3. Idempotency-Key 防重复提交。
  4. 数据库事务写入 comments,同时写入 Outbox。
  5. 事务提交后返回评论 ID。
  6. Worker 更新 Redis 热段、计数、通知、审核和搜索索引。

Redis 更新不放进数据库事务。数据库提交成功但 Redis 更新失败,可以靠 Outbox 重试;先写 Redis 再写数据库则可能产生不存在的脏评论。

读路径

首屏读取:

1
2
3
4
5
6
读帖子评论首屏
-> ZREVRANGE 取一级评论 ID
-> MGET/Pipeline 取评论体缓存
-> 缺失部分批量回源数据库
-> 回填短 TTL 评论体缓存
-> 每条一级评论附带少量回复预览

深页读取用游标,不用 offset:

1
2
3
4
5
6
7
8
SELECT *
FROM comments
WHERE post_id = ?
AND parent_id = 0
AND status = 1
AND (created_at, comment_id) < (?, ?)
ORDER BY created_at DESC, comment_id DESC
LIMIT 20;

评论列表边读边变,offset 容易重复和漏读。(created_at, comment_id) 是稳定游标,同一毫秒内也能打破并列。

为什么通常是二级评论

二级评论的动机和数据库是否能存树关系不大,它更像产品和工程之间的折中。

无限树的问题 二级评论的处理
移动端无限缩进不可读 一级观点 + 局部回复
递归读取和分页复杂 两个列表:帖子评论、一级评论回复
删除子树成本高 隐藏一级评论即可折叠一组回复
缓存 key 不可控 post_idroot_comment_id 两类 key
分片困难 一级评论天然成为回复分片锚点
审核范围不清 按一级评论聚合治理

parent_id 仍然保留直接回复语义,读路径只按 root_comment_id 拉回复列表。用户看到“回复某人”,系统处理的是“某个一级评论下的一条回复”。

难点

难点 处理
热帖首屏过热 Redis 热段缓存,本地 1-3 秒短缓存,深页回源
新评论刷屏 合并刷新,不强制每条立即推到在线客户端
删除审核 状态机 + Outbox 缓存失效 + tombstone 惰性清理
计数不一致 展示计数允许秒级延迟,后台校准
Redis Cluster 单 key Lua,跨 key 一致性交给 Outbox 重试

模式提炼:语义保留,结构压平

评论系统逻辑上有树,工程上按两个有序列表处理。root_comment_id 聚合讨论,parent_id 保留直接回复语义,Redis ZSet 只维护热段索引。

用例九:消息队列

问题

服务之间需要异步通信、削峰填谷、事件驱动和后台任务处理。Redis 能提供三种队列形态:List、Pub/Sub、Stream。

三种方案

方案 命令 能力 适合场景
List LPUSH + BRPOP 简单阻塞队列 轻量任务
Pub/Sub PUBLISH + SUBSCRIBE 实时广播,无积压 在线通知、广播
Stream XADD + XREADGROUP 持久化、消费者组、ACK 可靠事件流

List 示例:

1
2
LPUSH queue:email "{json}"
BRPOP queue:email 30

Stream 示例:

1
2
3
4
5
XGROUP CREATE stream:comment audit $ MKSTREAM
XADD stream:comment * comment_id 1001 post_id 123
XREADGROUP GROUP audit worker-1 COUNT 10 BLOCK 5000 STREAMS stream:comment >
XACK stream:comment audit 1678886400000-0
XPENDING stream:comment audit

取舍

Pub/Sub 不持久化,消费者离线就丢消息。List 简单,但缺少消费者组和 ACK。Stream 更接近专业 MQ,但堆积仍受 Redis 内存约束,不适合高吞吐、长周期、大规模可靠消息场景。重要业务事件优先 Kafka、RocketMQ 或数据库 Outbox。

用例十:地理位置服务

问题

附近的人、附近门店、3 公里内网点都需要按经纬度做距离和范围查询。

设计

Redis GEO 底层把经纬度编码到 ZSet score 中。

1
2
3
4
5
GEOADD geo:stores:hangzhou 120.1551 30.2741 store:1001
GEOADD geo:stores:hangzhou 120.1600 30.2800 store:1002

GEODIST geo:stores:hangzhou store:1001 store:1002 km
GEOSEARCH geo:stores:hangzhou FROMLONLAT 120.1551 30.2741 BYRADIUS 5 km WITHDIST COUNT 20 ASC

取舍

Redis GEO 适合圆形范围和距离排序,不适合复杂多边形围栏、路径规划、空间 JOIN。复杂地理查询应该使用 PostGIS 或搜索引擎的地理索引。

用例十一:UV 统计

问题

UV 统计需要按页面、天、活动统计独立访客数。精确 Set 会随用户规模线性增长。

设计

HyperLogLog 用固定小内存估算基数,误差可控。

1
2
3
4
5
6
PFADD uv:page:home:20250728 user:1
PFADD uv:page:home:20250728 user:2
PFCOUNT uv:page:home:20250728

PFMERGE uv:page:home:week31 uv:page:home:20250728 uv:page:home:20250729
PFCOUNT uv:page:home:week31

取舍

UV、独立 IP、活动覆盖人数通常可以接受小误差。抽奖去重、付费权益、风控名单不能用 HyperLogLog,因为它不能告诉某个用户是否存在,也不能删除单个成员。

用例十二:Bloom Filter 防穿透

问题

大量请求查询不存在的商品、用户或文章 ID,会绕过缓存直击数据库。空值缓存能缓解,但攻击面很大时仍然浪费存储。

设计

Bloom Filter 判断“某个元素是否可能存在”。返回不存在时一定不存在;返回存在时只是可能存在。

1
2
3
4
BF.RESERVE bf:product 0.0001 1000000
BF.ADD bf:product product:1001
BF.EXISTS bf:product product:1001
BF.EXISTS bf:product product:404

读路径:

1
2
3
4
请求 productId
-> Bloom Filter 判断不存在,直接返回空
-> 可能存在,继续查 Redis 缓存
-> 缓存未命中,再查数据库

取舍

Bloom Filter 有误判,不能删除普通元素。商品上架、下架、ID 迁移等场景要考虑重建过滤器,或使用支持删除的 Cuckoo Filter。

模式提炼:概率换空间

HyperLogLog 和 Bloom Filter 都是在可接受误差内换取数量级的空间节省。业务问题不要求精确成员集合时,可以考虑概率结构。

用例十三:分布式 ID

问题

多实例服务需要生成唯一 ID。数据库自增 ID 简单,但高并发下会集中到单点,也不利于多机房扩展。

设计

Redis INCR 能生成全局递增序号:

1
2
INCR id:comment
INCRBY id:order:segment 100

按业务和日期分段可以降低单 key 压力:

1
INCR id:order:20250728

应用层可组合成:

1
2
{biz}{date}{sequence}
ORD202507280000001

取舍

Redis ID 简单,但强依赖 Redis 可用性,跨机房和极高吞吐场景不如 Snowflake、号段服务或数据库序列。ID 一旦写入数据库,就不能因为 Redis 回滚而重复。

用例十四:签到、在线状态与二值状态

问题

签到、在线、功能开关、每日活跃等状态只有 0/1 两种取值。用 Set 保存用户 ID 可以做,但空间成本较高。

设计

Bitmap 用一个 bit 表示一个用户或某一天。

1
2
3
SETBIT sign:user:123:202507 27 1
GETBIT sign:user:123:202507 27
BITCOUNT sign:user:123:202507

全站日活可以按天建 Bitmap,offset 使用用户 ID:

1
2
3
4
SETBIT dau:20250728 123 1
BITCOUNT dau:20250728
BITOP AND active:3days dau:20250726 dau:20250727 dau:20250728
BITCOUNT active:3days

取舍

Bitmap 的前提是 offset 可控。用户 ID 如果极度稀疏,用最大用户 ID 作为 offset 会浪费空间。连续内部 ID、映射表或按用户 ID 分片都可以缓解。

Redis 选型边界

Redis 的边界比用例本身更重要。

需求 Redis 合适的条件 替代方案
缓存 数据可重建,读多写少 本地缓存、CDN
分布式锁 临界区短,重复执行代价可控 数据库事务、etcd、ZooKeeper
消息队列 轻量任务、短期积压 Kafka、RocketMQ、RabbitMQ
评论系统 热帖首屏、计数、限流、幂等 数据库 + Outbox + 搜索引擎
排行榜 单维实时排序 搜索引擎、OLAP
社交关系 普通用户集合运算 图数据库、推荐系统
GEO 圆形范围、距离排序 PostGIS、Elasticsearch
UV 统计 可接受误差 精确 Set、离线数仓
资金账务 不适合单独使用 Redis 关系型数据库、账务系统

生产设计原则

Redis 不是事实源

评论正文、订单、支付、库存扣减、审核状态、用户资产都应该有持久事实源。Redis 可以缓存、计数、排序、限流,但必须能从事实源或事件日志重建。

大 key 比慢查询更隐蔽

HGETALLSMEMBERS、大范围 ZRANGE 都可能阻塞 Redis。生产环境要限制单 key 成员数和 value 大小,扫描 hot key、big key,并对大集合做分片。

TTL 是数据模型的一部分

缓存、验证码、Session、限流窗口、幂等 token 都应有 TTL。没有 TTL 的缓存类 key 最终会变成内存泄漏。

Pipeline 减少往返,Lua 收紧原子边界

Pipeline 只减少网络往返,不提供事务原子性。Lua 可以保证单节点内多命令原子执行,但 Redis Cluster 下跨 slot 脚本不可作为默认方案。

一致性靠事件修复

数据库和 Redis 双写很难做到强一致。更常见的做法是数据库事务内写 Outbox,异步 worker 更新 Redis。读路径发现缓存缺失或脏状态时,可以回源修复。

Key 命名

Key 命名应表达业务域、实体、标识和子资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cache:user:{userId}
session:{token}
lock:order:{orderId}
idem:pay:{userId}:{token}
rl:comment:{userId}
rank:game:daily:{date}
delay:order:close
following:{userId}
followers:{userId}
cmt:post:{postId}:top:new
cmt:root:{rootCommentId}:reply:new
cmt:body:{commentId}
uv:page:{pageId}:{date}
sign:user:{userId}:{yyyyMM}
geo:stores:{city}
stream:comment

命名规则:

  • 使用冒号分隔层级
  • 避免无意义缩写
  • 把高基数字段放到后面,便于按前缀观察
  • Cluster 下需要同 slot 的 key 使用 hash tag,但不要为了原子性把热点全集中到一个 slot

面试速查表

需求关键词 模式 Redis 方案 必讲边界
缓存、热点、加速 KV + TTL SET key value EX ttl 穿透、击穿、雪崩
Session、验证码 短生命周期状态 String + TTL Redis 故障要能重新登录或重发
幂等、互斥、只能一个 原子占位 SET NX EX value 校验释放,不能替代事务
排行榜、Top N 分数数轴 ZSet 大榜单、同分、作弊回滚
延迟、定时、超时关闭 Score 即时间 ZSet + Lua 消费幂等,堆积受内存限制
限流、频率控制 滑动窗口 ZSet + Lua Cluster 单 key 原子边界
关注、共同好友 双向关系 Set + 交并差 大 V 大 key
评论列表 结构压平 DB + Redis ZSet 热段 二级评论、游标分页、审核
消息、异步、事件 消息流 Stream 高可靠高吞吐用专业 MQ
附近的人 地理索引 GEO 复杂空间查询用 PostGIS
UV、独立访客 概率换空间 HyperLogLog 不能做精确成员判断
缓存穿透 概率预判 Bloom Filter 误判和重建
签到、在线 位图压缩 Bitmap offset 稀疏问题
全局序号 原子递增 INCR / INCRBY 高可用和跨机房

组合题怎么答

复杂系统往往由多个 Redis 模式组合而成:

系统 Redis 模式组合 事实源
秒杀 原子占位 + Lua 扣减 + Stream/队列削峰 + 幂等 token 订单库、库存库
Feed 流 ZSet 时间线 + 热用户缓存 + 计数器 + 去重 Set 帖子库、关注关系库
评论系统 ZSet 热段 + 短 TTL 评论体 + 限流 + 幂等 + Outbox 修复 评论库
点赞系统 Set 去重 + 计数器 + 异步落库 点赞事实表
API 网关 滑动窗口 + 令牌桶 + 黑名单 TTL 配置中心、风控系统

Redis 在这些系统中的角色很一致:负责快、热、短、可重建;数据库和日志系统负责慢、全、准、可追溯。面试答案把这条边界讲清楚,Redis 就不会被讲成数据库替代品。