Redis 缓存的三种更新策略与一致性
缓存与数据库的双写一致,没有完美方案,只有适合业务容忍度的方案。先把三种基本策略摆出来。
Cache Aside(旁路缓存,最常用)
读:先查缓存,命中返回;未命中查库、写缓存、返回。 写:先更新数据库,再删除缓存。
为什么是删除而不是更新缓存?两个原因:一是并发更新时,两个线程的写缓存顺序可能与写库顺序相反,缓存留下旧值;二是缓存的值往往是多表聚合的结果,写时计算容易和读取逻辑不一致。
为什么先库后删?反过来(先删缓存再更新库)时,删除到更新完成之间的读请求会把旧值重新填进缓存,且旧值存活到过期。先更库后删缓存的窗口小得多,只在「删除失败」时有问题——用延迟双删或订阅 binlog 补偿。
// 读
$v = $redis->get($k);
if ($v === false) {
$v = $db->query(...);
$redis->setex($k, 3600, $v);
}
// 写
$db->update(...);
$redis->del($k);
Read/Write Through
应用只和缓存交互,缓存层负责同步读写数据库。逻辑集中在中间件(如 Twemproxy 时代的一些方案),应用代码干净,但实现复杂,自研成本高。
Write Behind(异步写回)
写只落缓存,定期批量刷回数据库。性能最高,但宕机会丢数据,适合点赞数、浏览量这类可容忍丢失的计数场景,不适合订单。
一致性到底要多强
绝大多数业务不需要强一致,需要的是「最终一致 + 窗口可控」。落地时问两个问题:
- 脏读窗口多大能接受?秒级可接受就用删除 + 过期时间兜底;
- 删除失败怎么办?重试队列、订阅 binlog(Canal 类)都是成熟方案。
两条纪律
- 缓存必须有 TTL。任何依赖「永远不失效」的缓存设计,出事时都没有退路;
- 缓存 value 尽量自包含。读取时不做二次拼装,删除时不猜依赖了哪些 key。缓存一旦变成关系网络,失效顺序就会变成噩梦。