说说缓存穿透,缓存击穿,缓存雪崩
- 缓存穿透:查了一个压根就不存在的数据(比如查 ID=-1),缓存没命中,数据库也没命中,导致每次请求都直接打在数据库上。
- 缓存击穿:某个热点数据(比如微博热搜第一名)刚好过期了,瞬间海量请求直接打在数据库上。
- 缓存雪崩:一大波数据同时过期,或者缓存服务直接挂了,导致所有请求像雪崩一样涌向数据库。
🕳️ 缓存穿透 (Cache Penetration)
核心特征:查询的数据在缓存和数据库中都不存在。
1. 场景还原
想象你是一个图书管理员(数据库),门口有个助手(缓存)。
有个捣蛋鬼一直问你:"有没有《哈利波特与量子力学》这本书?"
- 助手查了一下目录(缓存),说:"没有。"
- 捣蛋鬼不信,非要你进库房(数据库)查。
- 你进库房查了一圈,确实没有。
- 结果:因为库房里确实没这本书,你没法把这本书的信息告诉助手(无法回写缓存)。捣蛋鬼过一秒又问一遍,你只能再跑一趟库房。如果有一万个捣蛋鬼同时问不存在的书,你的库房大门会被挤爆。
2. 常见原因
- 恶意攻击:黑客故意构造不存在的 ID(如
-1,null, 随机 UUID)发起请求。 - 业务漏洞:前端传参错误,或者数据未同步。
3. 解决方案
- 方案一:缓存空对象(最常用)
- 做法:当数据库查询为空时,依然把这个"空结果"写入缓存(比如存个
null或特定字符串),并设置一个较短的过期时间(如 5 分钟)。- 效果:下次请求到缓存后直接返回,不需要再访问数据库。
- 做法:当数据库查询为空时,依然把这个"空结果"写入缓存(比如存个
- 方案二:布隆过滤器(布隆过滤器是一种空间效率极高的概率性数据结构,用于快速判断一个元素是否在一个集合中。)
- 做法:在请求到达缓存之前,先过一个"安检门"(布隆过滤器)。它告诉你这个 ID 可能存在 或者 一定不存在。
- 效果:如果布隆过滤器说"一定不存在",直接拦截,连缓存和数据库都不用查。
🔨 缓存击穿 (Cache Breakdown)
核心特征 :单个热点 Key 突然过期,高并发瞬间击穿。
1. 场景还原
还是那个图书管理员。
有一本《三体》特别火,大家都在看助手手里的复印件(缓存)。
- 突然,复印件过期失效了(Key 过期)。
- 这一瞬间,正好有 1 万个读者涌进来要看书。
- 大家发现助手没书了,于是 1 万人同时冲向库房(数据库)找你要原书。
- 结果:库房瞬间被挤爆,甚至导致数据库宕机。
2. 常见原因
- 热点数据:秒杀商品、微博热搜、突发新闻。
- 时间巧合:正好在访问高峰期,该数据的 TTL(生存时间)到了。
3. 解决方案
- 方案一:互斥锁(Mutex Lock)
- 做法:当发现缓存失效时,不要所有人都去查数据库。大家先抢一把锁(比如 Redis 的
setnx)。 - 效果:只有抢到锁的那 1 个人去查数据库并回写缓存,其他 9999 个人在门外等着(或者重试),等缓存写好了,他们直接从缓存读。
- 做法:当发现缓存失效时,不要所有人都去查数据库。大家先抢一把锁(比如 Redis 的
- 方案二:逻辑过期
- 做法:物理上不给缓存设置过期时间(永不过期),但在数据里存一个"逻辑过期时间戳"。
- 效果:读取时发现逻辑过期了,先返回旧数据(保证可用性),同时开启一个异步线程去后台更新数据。
🏔️ 缓存雪崩 (Cache Avalanche)
核心特征:大量 Key 同时过期 或 缓存服务宕机。
1. 场景还原
- 情况 A(同时过期):你为了省事,给库房里所有书的复印件都设置了"今晚 12 点过期"。结果 12 点一到,所有读者的复印件全失效了,所有人同时冲向库房。
- 情况 B(服务宕机):助手(Redis)突然累倒了(宕机),没法提供服务。所有读者只能直接冲进库房找你。
- 结果:这种流量冲击像雪崩一样,瞬间压垮数据库。
2. 常见原因
- 批量设置过期时间:代码里写死了
expireTime = 24h,导致所有数据在同一时刻失效。 - Redis 故障:服务器断电、网络中断、内存溢出导致 Redis 挂掉。
3. 解决方案
- 方案一:过期时间随机化
- 做法:在原有的过期时间基础上,加一个随机值。比如
基础时间 + 随机(0, 300秒)。 - 效果:让 Key 的过期时间分散开,避免"扎堆"失效。
- 做法:在原有的过期时间基础上,加一个随机值。比如
- 方案二:高可用架构
- 做法:部署 Redis 哨兵(Sentinel)或集群(Cluster)。
- 效果:主节点挂了,从节点立马顶上,实现自动故障转移,保证缓存服务不中断。
- 方案三:限流降级
- 做法:当数据库压力过大时,直接拒绝部分请求,或者返回默认值(比如"系统繁忙"),保住数据库的命。
本地缓存和Redis的区别了解吗?
本地缓存存在应用进程内,性能极高但多实例数据不一致、容量有限;Redis 是分布式缓存,数据全局一致、容量可扩展,但有网络开销;
本地缓存适合热点数据兜底、高性能要求的场景;对于读取频率极高、数据相对稳定、允许短暂不一致的数据,我优先选择本地缓存。Redis 适合分布式共享、海量数据、复杂操作的场景;对于需要实时同步、数据变化频繁、多个服务需要共享的数据,我会选择 Redis。
生产环境通常用「本地缓存 + Redis」多级架构,既保证性能,又保证一致性和可用性。
Redis 可以部署在多个节点上,支持数据分片、主从复制和集群。而本地缓存只能在单个服务器上使用。
1. 本地缓存适用场景
- 热点数据兜底:比如秒杀场景,将爆款商品缓存到本地,防止 Redis 宕机导致缓存雪崩;
- 高频读、低频写的静态数据:如地区编码、字典表、接口固定配置;
- 性能极致要求的场景:如核心接口响应时间要求 < 10ms,本地缓存可减少网络耗时;
- 分布式缓存的前置缓存:先查本地缓存,未命中再查 Redis,减少 Redis 请求量。
2. Redis 缓存适用场景
- 分布式场景:多应用实例共享缓存(如用户登录态、购物车);
- 海量数据缓存:超出单机内存容量的缓存(如商品列表、订单记录);
- 需要复杂操作的场景:分布式锁、限流、排行榜、计数器(如点赞数统计);
- 数据一致性要求高的场景:如商品价格、库存,需全局统一的缓存值。
3. 本地缓存数据不一致怎么解决?
① 设置极短过期时间(如 1 分钟),兜底数据一致性;② 核心数据更新时,通过 MQ / 事件通知所有实例更新本地缓存;③ 禁用本地缓存的写操作,仅通过 Redis 回写。
来说说你们项目里的缓存设计,怎么保证性能、一致性和高可用?
我们项目采用本地缓存加 Redis 二级缓存架构。系统启动和大促前会做缓存预热,避免冷启动压力。更新采用先更库、再删缓存的策略,配合过期时间和 Canal 保证最终一致。针对穿透、击穿、雪崩,分别用布隆过滤器、分布式锁、过期时间打散来防护。同时做了限流、降级、多级缓存兜底,保证在极端情况下核心业务依然可用。
具体如下:
一、整体架构:多级缓存设计
我们项目采用 本地缓存(Caffeine)+ 分布式缓存(Redis) 两级缓存架构:
- 读请求优先查本地缓存
- 速度最快,减少 Redis 压力
- 本地未命中再查 Redis
- Redis 未命中再查数据库
- 查到后回写到 Redis + 本地缓存
目的:
- 抗更高并发
- 降低 Redis 网络开销
- Redis 故障时本地缓存可以兜底,防止雪崩
二、缓存预热(冷启动保护)
为了避免系统刚上线或 Redis 重启后,大量请求直接打穿数据库,我们做了缓存预热:
- 项目启动时预热
- 用
ApplicationRunner在服务启动完成后,加载字典、配置、热点商品、类目等基础数据
- 用
- 定时预热
- 每天凌晨通过定时任务刷新静态数据缓存
- 大促手动预热
- 提供后台管理接口,手动触发秒杀商品、活动页缓存预热
- 分批加载
- 避免一次性加载太多数据导致启动慢、数据库压力大
预热后,系统一上线就能直接读缓存,不会出现冷启动性能尖刺。
三、缓存更新策略(保证一致性)
我们采用 Cache Aside 模式,这是业界最成熟、最推荐的方案:
- 查询:命中即返回,未命中查库并回填缓存
- 更新:先更新数据库,再删除缓存
- 不更新缓存,避免并发覆盖问题
- 删除缓存后,下一次查询自动拉最新数据
为了保证极端场景下的一致性:
- 给缓存加过期时间兜底
- 关键数据通过 Canal 监听 binlog 异步删除缓存,确保最终一致
四、缓存三大问题防护(穿透、击穿、雪崩)
1. 缓存穿透
- 前端 + 后端参数校验,过滤非法 ID
- 对不存在的数据缓存空值,并设置短过期
- 高并发恶意攻击场景使用布隆过滤器拦截
2. 缓存击穿
- 秒杀、热点商品设置永不过期
- 并发查询用分布式锁,只允许一个请求去查数据库
3. 缓存雪崩
- 过期时间加随机值打散,避免同时失效
- 多级缓存兜底
- Redis 使用哨兵 / 集群保证高可用
- 接口做限流 + 降级,保护数据库
五、缓存降级与兜底(高可用保障)
极端情况比如:
- Redis 集群宕机
- 网络抖动
- 数据库压力过大
我们会启用缓存降级策略:
- 核心接口直接返回本地缓存旧数据,保证可用
- 非核心接口直接返回默认值 / 静态页面
- 限流放开,避免流量压垮服务
- 监控告警,自动或人工恢复后关闭降级
保证核心业务可用,不雪崩、不宕机。