首页 / Redis 入门教程 / 缓存设计与典型应用

Redis 入门教程

缓存设计与典型应用

本教程共 40 篇 · 第 39 篇 · 更新于 2026-08-02

缓存设计缓存穿透缓存击穿缓存雪崩分布式锁限流消息队列

39. 缓存设计与典型应用

如果说前面 38 章讲的是「Redis 怎么用」,本章讲的是「Redis 怎么用好」。「码上学」认为,缓存是 Redis 最常见的身份,但用错缓存带来的故障(数据库被打垮、数据不一致)也比比皆是。本章聚焦四个高频实战主题:缓存三大经典问题、分布式锁、限流、消息队列选型。点到为止,给出可落地的 redis-cli 示例与设计思路。

本节目标

  • 识别并应对缓存穿透、击穿、雪崩三大问题。
  • SET key value NX EX 实现安全的分布式锁,并用 Lua 释放。
  • 基于计数器实现简单的接口限流。
  • 在 List / Pub-Sub / Stream 三种 MQ 方案间做出正确取舍。
  • 理解缓存更新策略与一致性要点。

一、缓存更新与一致性

最常用的是 Cache Aside(旁路缓存) 模式:读时先查缓存,未命中查库并回写;写时先更新数据库,再删除缓存(而非更新缓存),让下次读自然回填。删缓存而非更新,避免了「并发写导致缓存与库不一致」的多数情况。注意「先删缓存再更库」与「先更库再删缓存」各有极小窗口风险,生产中常配合「延迟双删」或消息通知来进一步收敛。

二、缓存三大经典问题

2-1 缓存穿透(Penetration)

查询一个数据库和缓存都不存在的数据,请求每次都穿透到数据库。恶意刷不存在的 id 就能压垮数据库。

解法一:布隆过滤器(Bloom Filter) 在访问缓存前先判断 key 是否可能存在,不存在直接拒绝。Redis 8.x 内置概率类模块(见第 15 章),可直接用 BF.ADD/BF.EXISTS;也可在应用层用 RedisBitmap 自行实现。

解法二:缓存空对象,把「查无此物」也写进缓存并给较短过期:

127.0.0.1:6379> SET user:999999 "" EX 60
OK

注意空值要设短 TTL,避免数据库后来真插入了却长期读到空。

2-2 缓存击穿(Breakdown)

某个热点 key 突然过期,瞬间大量并发请求同时打到数据库重建缓存。

解法一:互斥锁,只允许一个线程重建缓存,其余等待。用 SET NX 抢锁:

127.0.0.1:6379> SET lock:rebuild:hotkey my_token NX EX 10
OK

抢到锁的线程查库、回写缓存、再释放锁;没抢到的短暂重试读缓存。

解法二:热点 key 逻辑过期 / 永不过期,后台异步刷新,避免集中失效。

2-3 缓存雪崩(Avalanche)

大量 key 同时过期,或 Redis 整体宕机,请求洪流直接冲向数据库。

解法

  • 过期时间加随机抖动,分散失效时刻:expire = base + random(0, 300)
  • 多级缓存:本地缓存(如 Caffeine)+ Redis + 数据库,Redis 挂了还有本地兜底。
  • 高可用架构:主从 + 哨兵或 Cluster(见第 26~29 章),避免单点整体不可用。
  • 熔断降级:数据库压力过大时拒绝部分非核心请求,保护主链路。

三、分布式锁:SET NX EX

分布式锁用于多进程/多机之间互斥访问共享资源(如抢券、防重复提交)。Redis 因单线程与原子命令成为天然选择。正确姿势是一条命令同时设值 + 过期,避免「SET 后崩溃来不及 EXPIRE」导致死锁:

127.0.0.1:6379> SET lock:order:1001 my_unique_token NX EX 30
OK
  • NX:仅当 key 不存在才设置,保证互斥。
  • EX 30:30 秒自动过期,防止持有者崩溃后锁永不释放。
  • my_unique_token:持锁者随机值,释放时用于校验,避免误删别人的锁。

业务执行完,必须用 Lua 脚本原子释放(先比对 token 再删除),不能先 GETDEL(两步之间锁可能已易主):

127.0.0.1:6379> EVAL "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock:order:1001 my_unique_token
(integer) 1

关于 RedLock:Redis 官方曾提出在多独立实例上获取多数锁的 RedLock 算法,但存在时钟假设争议。对强一致要求极高的场景,社区更推荐 ZooKeeper / etcd;普通缓存互斥用单实例 SET NX EX 已足够。

四、限流:计数器窗口

最简单的固定窗口限流:对每个用户/接口用计数器,配合过期时间实现「每秒最多 N 次」。

127.0.0.1:6379> SET rate:user:1:window1 0 NX EX 1
OK
127.0.0.1:6379> INCR rate:user:1:window1
(integer) 1
127.0.0.1:6379> INCR rate:user:1:window1
(integer) 2

INCR 返回值超过阈值(如 100)即拒绝请求;窗口随 key 过期自动滚动。更平滑的「滑动窗口」可用 ZSET 记录时间戳 + ZREMRANGEBYSCORE 清理旧请求后计数。生产级限流也可借助 Redis 模块(如 redis-cell 的 CL.THROTTLE)获得令牌桶语义。

五、消息队列选型

Redis 能胜任多种消息场景,但三种原语取舍不同:

5-1 List:简单队列

RPUSH 生产、LPOP/BRPOP 消费,BRPOP 阻塞避免空轮询:

127.0.0.1:6379> RPUSH queue:tasks "task-1"
(integer) 1
127.0.0.1:6379> BRPOP queue:tasks 0
1) "queue:tasks"
2) "task-1"

缺点:无消费确认,消费者崩溃会丢消息;不支持多消费者组。

5-2 Pub/Sub:广播

PUBLISH 发布、SUBSCRIBE 订阅,适合 1:N 实时推送。缺点:消息不持久化,离线订阅者收不到;断连期间消息丢失。

5-3 Stream:可靠队列(推荐)

Redis 5.0 引入的 Stream 支持消费者组、消息确认(XACK)和待处理列表(PEL),消息不丢失,是 Redis 做可靠 MQ 的首选:

127.0.0.1:6379> XADD orders * product_id 1001 qty 2
"1764567890123-0"
127.0.0.1:6379> XREADGROUP GROUP g1 c1 COUNT 1 STREAMS orders >
1) 1) "orders"
   2) 1) 1) "1764567890123-0"
         2) 1) "product_id"
            2) "1001"
            3) "qty"
            4) "2"

消费成功后 XACK orders g1 1764567890123-0 确认,未确认的消息留在 PEL 可重投。

选型小结:轻量、可丢、广播 → Pub/Sub;简单 FIFO、能接受偶发丢 → List;要可靠、要消费者组、要重投 → Stream。

六、常见误区

  • 误区一:「缓存空对象不设 TTL。」会导致不存在的 key 长期占位,数据库插入后却读到旧空值。
  • 误区二:「分布式锁用 SETNX + EXPIRE 两条命令。」两步非原子,崩溃会死锁;务必用 SET ... NX EX 一条命令。
  • 误区三:「释放锁直接 DEL。」可能删掉别人刚抢到的锁;必须用 token 校验的 Lua 释放。
  • 误区四:「雪崩只靠加内存解决。」应从过期抖动、多级缓存、高可用三管齐下,单点扩容治不了整体宕机。

提示:本章正文统一以 Redis 8.x(最新稳定版) 表述。需补充的是,官方 redis.io 下载页另将 8.8 标注为 “Latest stable”,而 GitHub 上 redis/redis 的最新发布 tag 为 8.10.0;二者同属 8.x 系列,本章涉及的 SET NX EX、Stream、概率类型等命令对教材内容没有影响。