Redis 常被一句话概括为“内存数据库”,但在真实项目里,它的价值远不止把数据放进内存。
登录态、热点缓存、验证码、排行榜、接口限流、延迟任务、抢购库存、分布式锁……这些看似不相干的需求,背后都在处理同一类问题:如何用低延迟、可共享、可原子操作的状态,支撑高并发业务。 Redis 恰好提供了这组能力。
本文以 Node.js + TypeScript 为示例,梳理 Redis 在项目开发中最常见的使用方式。重点不只是“命令怎么写”,更是“什么场景该用、边界在哪里、怎样避免线上坑”。
示例使用
ioredis。不同客户端的 API 略有差异,但 Redis 的命令语义一致。
一、先建立正确认知:Redis 适合解决什么问题
Redis 的核心特征有三点:
- 数据主要存放在内存中,读写延迟很低;
- 支持字符串、哈希、列表、集合、有序集合、Stream 等多种结构;
- 单条命令天然具备原子性,并可借助 Lua 脚本完成多步原子操作。
因此,它尤其适合下面两类数据:
- 读多写少、可被短暂缓存的数据:例如商品详情、文章详情、配置项。
- 需要快速共享和协调的临时状态:例如会话、验证码、限流计数、锁、排队任务。
Redis 通常不是 MySQL、PostgreSQL 这类关系型数据库的替代品。订单、余额、用户资料等核心事实数据,仍应以持久化数据库为准;Redis 更适合作为加速层和协作层。
二、项目初始化:连接、序列化与 Key 规范
安装客户端:
1 | npm install ioredis |
封装一个可复用的连接:
1 | // src/lib/redis.ts |
业务对象写入 Redis 前通常需要序列化:
1 | type ProductCache = { |
Key 的命名建议
Key 是 Redis 中最容易被忽略的“数据模型”。推荐统一采用冒号分层:
1 | 业务域:资源:标识:附加维度 |
这样做有三个好处:可读性更强、方便按前缀统计与清理、不同模块不容易发生 Key 冲突。不要把密码、身份证号、完整 Token 等敏感内容直接拼进 Key。
三、最常见场景:缓存数据库查询结果
1. Cache Aside:旁路缓存模式
绝大多数业务缓存都可以用这一模式:
- 先查询 Redis;命中则直接返回。
- 未命中时查询数据库。
- 将查询结果写入 Redis,并设置过期时间。
1 | const CACHE_TTL_SECONDS = 5 * 60 |
更新时,推荐先更新数据库,再删除缓存:
1 | async function updateProduct(id: number, input: UpdateProductInput) { |
为什么不是“更新数据库后再更新缓存”?因为缓存结构通常不止一份:详情缓存、列表缓存、搜索缓存都可能受影响。删除可以让后续读请求自然重建,代码更容易保持正确。若对一致性要求很高,还应引入消息队列、订阅 binlog 或事务消息等机制,而不是只依赖一次 DEL。
2. TTL 不是可选项
除非是经过明确设计的常驻配置,否则缓存都应设置过期时间。没有 TTL 的缓存会逐渐占满内存,也会让陈旧数据长期存在。
对于大量热点 Key,不要把 TTL 设成同一个精确时间;可以增加随机抖动,降低同一时刻集体失效的风险:
1 | function withJitter(baseSeconds: number, jitterSeconds = 60) { |
四、缓存三大问题:穿透、击穿与雪崩
这三个概念经常一起出现,关键是分清“谁在失效、影响范围多大”。
| 问题 | 典型原因 | 风险 | 常见处理方式 |
|---|---|---|---|
| 缓存穿透 | 请求不存在的数据 | 每次都落到数据库 | 缓存空值、布隆过滤器、参数校验 |
| 缓存击穿 | 一个热点 Key 在失效瞬间被大量请求访问 | 大量请求同时回源 | 互斥锁、逻辑过期、热点预热 |
| 缓存雪崩 | 大批 Key 同时失效或 Redis 故障 | 下游数据库被压垮 | TTL 打散、多级缓存、限流与降级 |
1. 缓存穿透:缓存空值
对于确实不存在的 ID,也可以缓存一个短期占位符:
1 | const NULL_VALUE = '__NULL__' |
空值 TTL 应更短,以免新建数据后仍被旧占位符遮挡。对于海量、明显非法的 ID 请求,可在缓存前加布隆过滤器,先快速判断“这个 ID 理论上是否可能存在”。
2. 缓存击穿:用互斥锁合并回源请求
热点 Key 失效时,只允许一个请求查询数据库并重建缓存,其他请求短暂等待或返回降级结果:
1 | async function getHotProduct(id: number) { |
这段代码用于说明思路。生产环境的锁释放需要“只释放自己的锁”,后文会给出正确写法。
五、登录态、验证码与一次性 Token
Redis 的过期能力非常适合保存临时凭证。
1. 短信或邮箱验证码
1 | const key = `verify-code:login:${phone}` |
注意:验证码本身不应写日志;发送频率还必须额外限流,不能只依靠验证码 TTL。
2. Token 黑名单与会话管理
JWT 天生无状态,但“退出登录后立即失效”是常见需求。可以把 Token 的唯一标识 jti 放入黑名单,TTL 设为 Token 的剩余有效期:
1 | await redis.set(`auth:blacklist:${jti}`, '1', 'EX', remainingSeconds) |
如果采用服务端 Session,则可以直接使用 session:{sessionId} 保存用户 ID、设备信息与过期时间。无论哪种方式,都要注意会话固定攻击、Token 泄露,以及多端登录策略。
六、排行榜、去重与共同关系:Set 和 ZSet
1. 用 ZSet 做排行榜
有序集合(Sorted Set)会按分数自动排序,非常适合积分、热度、游戏分数等场景:
1 | const key = 'rank:weekly:2026w28' |
ZREVRANGE 返回的数组是“成员、分数、成员、分数”的交替形式,业务层通常应转换成对象再返回给前端。
2. 用 Set 做点赞与去重
1 | const key = 'article:liked-users:1001' |
Set 也能高效计算交集,例如“共同关注”“共同好友”。但成员数非常大时,需谨慎使用 SMEMBERS、SINTER 等可能返回或计算大量数据的命令。
七、接口限流:固定窗口与滑动窗口
最简单的固定窗口计数器可以通过 INCR 实现:
1 | async function allowRequest(userId: string) { |
这里有一个小细节:INCR 与 EXPIRE 是两条命令。应用若在中间崩溃,Key 可能没有过期时间。更严谨的做法是使用 Lua 脚本,将递增和首次设置过期合为一个原子操作:
1 | const script = ` |
固定窗口实现简单,但会有边界突刺:用户在 12:00:59 和 12:01:00 分别发送 100 次请求,实际两秒内 200 次却都可能通过。对登录、下单等更敏感的接口,可使用 ZSet 记录请求时间戳实现滑动窗口,或采用令牌桶算法。
八、分布式锁:能用,但必须正确使用
当服务部署多个实例时,进程内的 mutex 只能锁住当前进程。若多个实例可能同时执行同一任务,例如“同一订单只能支付一次”或“定时任务只由一台机器执行”,才需要考虑 Redis 分布式锁。
加锁要使用 SET key value NX PX milliseconds:
1 | import { randomUUID } from 'node:crypto' |
锁值必须是唯一标识,释放锁时必须校验这个值。否则 A 的锁过期后,B 获得新锁,A 再执行一个无条件 DEL 就会错误地删除 B 的锁。
1 | const unlockScript = ` |
还要认识到:锁不是业务正确性的唯一保障。比如防止重复扣款,数据库唯一约束、订单状态机和支付渠道幂等号仍然是最后防线。锁只是降低并发冲突的手段。
九、异步任务:List、Pub/Sub 与 Stream 如何选择
Redis 可承载轻量任务通信,但不同结构适合不同目标:
| 方式 | 适合场景 | 关键限制 |
|---|---|---|
List(LPUSH / BRPOP) |
简单的单消费者任务队列 | 消费确认、失败重试能力需要自己补齐 |
| Pub/Sub | 在线通知、缓存失效广播 | 消息不持久,离线订阅者收不到 |
| Stream | 需要消费组、确认与重放的事件流 | 运维和积压治理复杂度更高 |
一个基础队列示例:
1 | // 生产者 |
涉及订单、支付、发券等不能丢的任务时,不应把“成功从队列取出”直接等同于“任务执行成功”。需要设计幂等键、重试、死信队列和可观测性;许多团队会直接选择 BullMQ、RabbitMQ 或 Kafka 等更匹配的基础设施。
十、性能与稳定性:这些坑比命令更重要
1. 避免大 Key 与危险命令
一个 Key 中塞入过大的 JSON、几十万成员的集合,都会造成网络传输、序列化和 Redis 主线程阻塞。线上排查 Key 时避免使用 KEYS *,它会遍历整个键空间;使用渐进式的 SCAN:
1 | redis-cli --scan --pattern 'product:detail:*' |
同样要谨慎执行 SMEMBERS、LRANGE 0 -1、超大范围 ZRANGE。为集合设计分页和上限,比事后优化更重要。
2. 设置合理的内存淘汰策略
Redis 内存有限,达到 maxmemory 后的行为由淘汰策略决定。纯缓存场景常见 allkeys-lru 或 allkeys-lfu;如果部分 Key 绝不能淘汰,不能只依赖“同一个 Redis 实例 + 默认配置”来保证。应根据业务隔离实例、设置监控告警,并明确哪些数据可以丢失。
3. 不要把 Redis 当作绝对可靠的事务数据库
Redis 支持持久化,但 RDB 快照与 AOF 策略决定了故障时可能丢失多少数据。对于库存、金额、订单状态等关键数据,要在关系型数据库中保留权威记录,并通过事务、唯一索引、幂等设计保证正确性。
4. 监控四类指标
- 可用性:连接错误、超时、主从切换次数;
- 性能:命令耗时、慢查询、CPU、网络带宽;
- 容量:内存使用率、Key 数量、淘汰次数、过期次数;
- 业务效果:缓存命中率、限流拦截数、队列积压量、锁竞争率。
没有命中率和淘汰指标,就很难判断“Redis 到底是在加速业务,还是已经成为瓶颈”。
十一、一个实用的落地清单
在为新需求引入 Redis 前,可以快速检查以下问题:
- 这份数据的权威来源是什么?Redis 故障或丢失后能否重建?
- Key 命名、TTL、最大体积和预计数量是否已定义?
- 缓存未命中、热点失效和 Redis 不可用时,接口如何降级?
- 写操作后如何保证缓存与数据库最终一致?
- 是否需要原子性?单条命令够不够,还是需要 Lua 或数据库事务?
- 是否存在重复请求?幂等键、唯一索引和状态机是否到位?
- 是否具备命中率、内存、慢命令、错误率等监控?
结语
Redis 的学习路径不应停在 SET、GET、EXPIRE。真正决定项目质量的,是能否把数据生命周期、并发竞争、失败恢复和监控治理一起纳入设计。
一个成熟的 Redis 使用方案通常很朴素:核心数据仍由数据库兜底;缓存带 TTL;更新主动失效;热点有保护;关键操作可幂等;大 Key 与全量命令被严格禁止;出现故障时有降级和监控。把这些基础原则做好,Redis 才会成为高并发项目的助推器,而不是新的不确定性来源。