多级缓存(本地 + Redis)
为什么需要多级缓存?
单一缓存(如只用 Redis)在超高并发场景下可能成为瓶颈(网络 IO、序列化开销、Redis 单点压力),多级缓存通过“就近取数据”来提升性能、降低延迟、减轻后端压力。
多级缓存结构(典型架构)
| |
注意:本地缓存是“进程级”的,不同服务实例之间的本地缓存不共享。
本地缓存常用方案
- Python 中:
functools.lru_cache、cachetools(支持 TTL、最大容量)、diskcache(可持久化到磁盘) - Java 中:Caffeine、Guava Cache
- 特点:访问速度极快(微秒级),但容量有限、数据不一致风险高
分布式缓存(Redis)
- 支持集群、持久化、丰富数据结构
- 适合共享数据、大容量缓存
- 缺点:网络开销(毫秒级)、序列化成本、单点/集群运维复杂度
多级缓存更新策略(关键!)
方案 1:穿透式写(Write-Through)
- 数据更新时,同时更新本地缓存 + Redis + 数据库
- 优点:一致性较好
- 缺点:写性能差,失败处理复杂
方案 2:回写式(Write-Back / Write-Behind)
- 只写本地缓存,异步批量刷到 Redis/DB
- 优点:写性能高
- 缺点:宕机丢数据,一致性差
方案 3:失效策略(Cache-Aside) ← 最常用
- 读:先读本地 → 未命中 → 读 Redis → 未命中 → 读 DB → 回填本地+Redis
- 写:先更新 DB → 再删除本地缓存 + 删除 Redis 缓存
- 优点:简单、通用、写性能好
- 缺点:短暂不一致(最终一致),需处理并发写竞争
建议:Cache-Aside + 异步延迟双删 + 监听 Binlog 补偿,是互联网公司主流方案
缓存三大经典问题及解决方案
缓存穿透(Cache Penetration)
定义:
查询一个根本不存在的数据(如 ID=-1 或 不存在的用户),每次 DB 都查不到,缓存也不存储 → 所有请求打到 DB,压垮数据库。
解决方案:
方案 1:缓存空值(Null Caching)
| |
方案 2:布隆过滤器(Bloom Filter)
- 在缓存层前加一个布隆过滤器,快速判断“某个 key 是否可能存在”
- 如果布隆过滤器说“不存在”,直接返回,不查缓存和 DB
- 优点:内存占用小,查询极快
- 缺点:有误判率(可能把存在的说成不存在 → 可接受),不支持删除
方案 3:接口参数校验 + 黑名单
- 对明显非法请求(如负 ID、超长字符串)直接拦截
- 记录恶意 IP 或参数,加入黑名单
缓存击穿(Cache Breakdown)
定义:
某个热点 key 过期瞬间,大量并发请求同时发现缓存失效,全部打到数据库 → 数据库瞬时压力暴增。
解决方案:
方案 1:热点数据永不过期(逻辑过期)
- 不设 TTL,但在 Value 中加入“逻辑过期时间”
- 后台异步线程定期刷新缓存
- 请求时若发现逻辑过期,则触发异步更新,当前请求仍返回旧值
方案 2:互斥锁重建缓存(Mutex Lock)
| |
方案 3:随机过期时间(防集体失效)
- 对同类热点数据,设置 TTL 时加一个随机值(如 3600 ± 300 秒)
- 避免大量 key 同时过期
缓存雪崩(Cache Avalanche)
定义:
大量缓存 key 在同一时间过期(如 Redis 宕机重启、或批量设置相同 TTL),导致所有请求穿透到 DB → 数据库被打挂。
和“击穿”区别:击穿是单个热点 key 失效,雪崩是“大面积集体失效”
解决方案:
方案 1:过期时间随机化(同击穿方案 3)
| |
方案 2:搭建高可用 Redis 集群
- 主从 + 哨兵 / Redis Cluster,避免单点故障
- 持久化(RDB+AOF)保障重启后快速恢复
方案 3:服务熔断 + 降级
- 当检测到 DB 压力过大,自动降级:返回默认值、错误页、排队等待
- 使用 Hystrix / Sentinel 实现熔断
方案 4:多级缓存 + 本地缓存兜底
- 即使 Redis 全挂,本地缓存还能撑一段时间(虽然数据可能旧)
方案 5:缓存预热(Warm Up)
- 系统启动或大促前,提前加载热点数据到缓存
- 可通过脚本或定时任务触发
缓存一致性(Cache Consistency)
这是最难的部分 —— 如何保证“缓存中的数据”和“数据库中的数据”在各种并发场景下保持一致?
常见场景
- 用户修改了数据 → 如何让缓存失效?
- 多服务实例并发修改 → 如何避免脏数据?
- 主从 DB 同步延迟 → 缓存读到旧数据怎么办?
解决方案与最佳实践
Cache-Aside + 延迟双删(推荐)
| |
💡 为什么延迟?防止“读请求在你更新 DB 前读了旧缓存,又在你删缓存后把旧值回填”
监听数据库 Binlog(终极方案)
- 使用 Canal / Debezium 监听 MySQL Binlog
- 一旦数据变更,异步发送消息(如 Kafka)通知缓存服务删除对应 key
- 优点:解耦、可靠、最终一致
- 缺点:架构复杂,有延迟
设置较短 TTL + 最终一致
- 不追求强一致,允许短暂不一致(如 5 秒内)
- 适用于对一致性要求不高的场景(如商品浏览量、文章点赞数)
读写锁 or 分布式锁(慎用)
- 写操作前加锁,阻塞所有读(性能差,不推荐高并发场景)
版本号 / 时间戳比对
- 缓存 Value 中带版本号,读取时与 DB 比对,不一致则刷新
- 适合对一致性要求极高的场景(如金融)
总结对比表
| 问题类型 | 原因 | 解决方案关键词 | 一致性级别 |
|---|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 布隆过滤器、空值缓存、参数校验 | - |
| 缓存击穿 | 热点 key 过期,大量并发重建 | 互斥锁、逻辑过期、随机 TTL | 强一致(锁) |
| 缓存雪崩 | 大量 key 同时失效或 Redis 宕机 | 随机 TTL、集群高可用、熔断降级 | 最终一致 |
| 缓存一致性 | DB 与缓存数据不一致 | 延迟双删、监听 Binlog、短 TTL | 最终一致(推荐) |
参考资料
- 《Redis 设计与实现》 — 黄健宏
- 《数据密集型应用系统设计》(DDIA)Chapter 5 & 7
- 美团技术博客《缓存穿透、击穿、雪崩区别和解决方案》
- 阿里云 Redis 最佳实践文档