缓存的价值
数据库的查询通常涉及磁盘 I/O,而缓存(如 Redis)把热点数据放在内存中,响应时间可以从毫秒级降到微秒级。
这能显著提升系统吞吐,并保护后端数据库。
基本读取流程
- 先查缓存;
- 命中则直接返回;
- 未命中则查数据库,写入缓存,再返回。
三个经典问题
一、缓存穿透(Penetration)
现象:查询一个根本不存在的数据。缓存里没有,数据库里也没有,每次请求都打到数据库。
如果有人用大量不存在的 ID 恶意请求,数据库可能被打垮。
解法:
- 缓存空值:数据库查不到时,也在缓存中存一个空标记(设置较短的过期时间)。后续请求直接命中缓存返回;
- 布隆过滤器:在缓存前加一层布隆过滤器,预先记录所有合法的 key。请求到来时先过过滤器,不存在的直接拒绝。
布隆过滤器的特点是:判断"不存在"一定准确,判断"存在"可能有误判。这个特性正好适合做前置过滤。
二、缓存击穿(Breakdown)
现象:某个热点 key 在缓存中过期的瞬间,大量并发请求同时发现缓存缺失,全部涌向数据库。
与穿透的区别:这个 key 是真实存在的,只是刚好过期了。
解法:
- 互斥锁(Mutex):只允许一个线程去数据库加载,其他线程等待或返回旧值;
- 逻辑过期:不在缓存中设置物理过期时间,而是在 value 中记录一个"逻辑过期时间"。发现逻辑过期后,异步刷新缓存,当前请求仍返回旧值。这样不会有大量请求同时涌向数据库;
- 热点 key 永不过期:对极热点的 key 不设置过期,通过后台任务主动更新。
三、缓存雪崩(Avalanche)
现象:大量 key 在同一时间集体过期,或者缓存服务整体宕机,导致所有请求直接打到数据库。
解法:
- 过期时间加随机值:避免大量 key 同时过期。比如在基础过期时间上加一个随机抖动;
- 多级缓存:本地缓存 + 分布式缓存,降低对单一缓存层的依赖;
- 缓存高可用:哨兵、集群、多副本;
- 熔断与限流:数据库压力过大时,拒绝部分请求,保护系统不整体崩溃;
- 预热:系统启动或大促前,主动把热点数据加载到缓存。
更新策略
Cache Aside(旁路缓存)
最常用的模式:
- 读:先读缓存,未命中读数据库并写入缓存;
- 写:先更新数据库,再删除缓存。
注意是"删除"而非"更新"缓存。原因:
- 更新缓存的代价高(可能要计算复杂结果),如果数据很少被读,就是浪费;
- 并发写时,直接更新缓存可能导致数据不一致。
为什么是"先数据库后缓存"
如果反过来(先删缓存再更新数据库),可能出现:
- 线程 A 删除缓存;
- 线程 B 读请求发现缓存缺失,查数据库(旧值),写入缓存;
- 线程 A 更新数据库;
- 结果:缓存中是旧值,数据库中是新值,不一致。
而"先更新数据库再删缓存"虽然也不能完全避免极端情况,但出现问题的概率要低得多(因为缓存的写入通常比数据库的写入快得多,步骤 2 很难插在 1 和 3 之间)。
一致性问题
要接受一个现实:引入缓存就必然引入一定程度的数据不一致。
大多数业务场景能接受短暂的不一致(最终一致性)。如果要求强一致,缓存的价值就大打折扣,需要更仔细的设计(如分布式锁、事务消息)。
说明:本文为缓存设计的公开知识整理。