缓存策略:本地缓存 vs 分布式缓存(Redis)
高并发接口里,缓存是提速的第一板斧。我个人习惯把缓存分成两类:本地缓存和分布式缓存。选错了,系统要么扛不住压力,要么数据乱成一锅粥。这一节我带你彻底搞懂它们的区别。
本地缓存的底牌
本地缓存其实就是把数据塞到应用进程的内存里。比如 Guava Cache、Caffeine、EhCache。你想想看,数据就在自己家里,读取速度约等于 CPU 寄存器到 L1 缓存的延迟——毫秒级,甚至纳秒级。适合存那些冷热分明、几乎不变的数据。
// Caffeine 示例(Spring Boot 自动配置)
@Bean
public Cache<String, UserProfile> userProfileCache() {
return Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats()
.build();
}
本地缓存的三道坎
- 容量有限——JVM 堆就那么大,几十 GB 顶天了。
- 数据一致性问题——每个节点自己玩自己的,别的节点改了数据,这头还是旧值。
- 重启即丢失——应用重启,缓存清空。高峰时集体回源能把数据库打崩。
我在项目里遇到过这个坑:有一回业务促销,配置中心改了限流阈值,本地缓存写了 10 分钟过期。结果前 5 分钟新配置根本没生效,把流量全放进了核心链路……后来我调整成了弱引用 + 异步刷新,才算稳住。
分布式缓存(Redis)的优势与软肋
Redis 是分布式缓存的事实标准。数据不在进程里,而在独立集群中。好处很明显:
- 容量可以水平扩展,几百 GB 甚至 TB 级
- 所有服务实例共享同一份缓存数据,一致性天然比本地缓存好
- 持久化机制让你不怕重启丢数据
但代价也不小。一次 Redis 读取的网络延迟大概 1~5ms,比本地缓存慢了 1000 倍。另外,缓存穿透、雪崩、击穿这些高并发病,都跟 Redis 强相关。
核心对比表
| 维度 | 本地缓存 | 分布式缓存(Redis) |
|---|---|---|
| 延迟 | < 0.1ms | 1~5ms |
| 容量 | 堆大小(通常 ≤ 64GB) | 集群 TB 级 |
| 一致性 | 最终一致(需自己同步) | 强一致(单 key 串行化) |
| 持久化 | 无 | RDB / AOF |
| 跨进程共享 | 不支持 | 原生支持 |
| 运维成本 | 无 | 需要单独部署、监控 |
| 适用场景 | 配置、字典、热点 KOL 信息 | 用户会话、库存、排行榜 |
混合缓存架构:两全其美的方案
实际生产里,我极少只用一种。更常见的做法是本地缓存作为 L1,Redis 作为 L2。流程大概是:
- 请求先查 L1(本地),命中直接返回。
- 未命中,查 L2(Redis),异步回写 L1。
- Redis 也未命中,回源数据库,双写 Redis 和本地。
这样做的好处很明显:热点数据被 L1 挡住了 90% 的流量,Redis 的 QPS 压力骤降,数据库几乎只扛穿透流量。我曾在某电商大促中把接口 RT 从 80ms 降到 6ms,核心就是这套两级缓存。
L1 过期时间 < L2 过期时间(例如 L1 30秒,L2 5分钟)。这样可以确保 L1 先失效,新数据有机会从 L2 拿到,避免 L2 过期后所有流量直接跳到数据库。
何时该单独上本地缓存?
- 数据几乎不变,且单个实例就够了(例如静态配置)
- 对延迟极其敏感,不能忍受一次网络 IO(例如行情价格)
- Redis 不可用时的降级兜底策略
何时该放弃本地缓存?
- 数据频繁更新,跨节点一致性要求高(例如多人抢购库存)
- 缓存容量超过单节点堆大小
- 微服务实例太多,本地缓存导致内存资源浪费
嗯,这里要注意一点:本地缓存不是越全越好。我曾经把用户信息全部塞进本地缓存,结果每个节点 8GB 堆,100 个实例就是 800GB,Redis 集群才 200GB。后来我改成只缓存 Top 10% 的热点用户,用滑动窗口动态调整,内存占用降了 80%。
缓存穿透、击穿、雪崩的统一解法
| 问题 | 表现 | 解法 |
|---|---|---|
| 缓存穿透 | 查数据库里都没有的数据,缓存永远不会命中 | 布隆过滤器 + 缓存空值(加短过期) |
| 缓存击穿 | 热点 key 过期瞬间,大量并发涌入数据库 | 本地缓存 + 互斥锁 / 异步刷新 |
| 缓存雪崩 | 大量 key 同时过期,或 Redis 宕机 | 过期时间加随机值 + Redis 集群 + 本地缓存兜底 |
知识体系:缓存选型决策流程
以下这张图概括了本文的核心决策路径——从数据特征出发,判断用本地缓存还是分布式缓存,以及如何组合。
拿到这张图你就清楚了:没有银弹。我自己的做法是:先用日志和监控分析接口的读写比率、数据变化频率、字节大小,然后参照上图画个草图,再结合业务允许的延迟和一致性要求,做出选择。