3. 缓存读写模式设计

缓存读写模式决定了应用代码、缓存层与数据库之间的职责边界。选错模式,往往不是“性能差一点”,而是出现脏数据、写放大、缓存击穿或数据丢失。本节把四种主流模式放在同一张“读写路径图”上对比,再落到高并发场景下的取舍依据。

一、四种模式的读写路径

先明确一个约定:下文所有“读/写”均以业务请求方向为参照——读指业务需要获取数据,写指业务需要修改数据;缓存与数据库之间的同步方向单独说明。

业务应用 缓存层 (Redis) 数据库 (MySQL) Cache-Aside(旁路缓存) 应用代码 Redis MySQL 读:先查缓存 未命中→查库→回填缓存;写:先写库→删除缓存 Read-Through(读穿透) 应用代码 缓存组件 MySQL 读请求 未命中自动加载 应用只与缓存交互,回源逻辑封装在缓存组件内 Write-Through(写穿透) 应用代码 Redis MySQL 写请求 同步写库 缓存与库在同一写事务内更新,强一致但写延迟高 Write-Behind(写回/异步写) 应用代码 Redis + 异步队列 MySQL 写请求 批量/延迟落库 写延迟最低,但宕机可能丢最近写入,需补偿与幂等

二、Cache-Aside:最常用,也最容易写错

Cache-Aside 由应用代码同时管理缓存和数据库,是 Redis 场景下最普遍的模式。读路径是“先查缓存,命中直接返回;未命中查库,再把结果写回缓存”。写路径的争议最大:推荐“先更新数据库,再删除缓存”,而不是“更新数据库后更新缓存”。原因是并发写时,两次更新缓存的顺序可能颠倒,导致缓存长期停留在旧值;删除缓存则让下一次读自然回源,把不一致窗口压缩到一次读的时长。

即便如此,仍存在经典的“读旧写新”竞态:读线程 A 未命中,查库得到旧值 v1;此时写线程 B 更新库为 v2 并删除缓存;A 随后把 v1 写回缓存,缓存就脏了。缓解手段有三类:一是给缓存设置合理 TTL(例如 300 秒),把最坏不一致限制在 TTL 内;二是延迟双删,写后延迟约 500ms 再删一次;三是用 Canal 订阅 binlog 异步删除,把删除动作与业务解耦。

// Cache-Aside 读
String get(String key) {
    String v = redis.get(key);
    if (v != null) return v;
    v = db.query(key);              // 回源
    if (v != null) redis.setex(key, 300, v);
    return v;
}
// Cache-Aside 写:先库后删
void update(String key, String val) {
    db.update(key, val);
    redis.del(key);                 // 删除而非更新
}

三、Read-Through:把回源逻辑收进缓存层

Read-Through 与 Cache-Aside 的读路径结果相似,区别在职责归属:应用只调用缓存,缓存组件在未命中时自己回源数据库并回填。它的价值是统一回源入口,避免每个业务方各写一套“查库+回填+空值处理”的代码,减少漏加 TTL、漏防穿透的概率。代价是需要缓存组件支持 loader 扩展,且回源逻辑与缓存实现耦合,排查问题时要同时看两侧日志。适合读多写少、数据访问模式统一、团队希望收敛缓存治理的场景。

四、Write-Through:一致优先,写延迟换稳定

Write-Through 在写请求时同步更新缓存与数据库,通常由缓存组件保证两者在同一逻辑写操作内完成。它的优点是缓存几乎不会出现“库新缓存旧”,读路径命中率高且稳定;缺点是每次写都要等数据库落盘,写吞吐受数据库限制,且对冷数据也会产生一次无意义的缓存写入(可配合“写时判断是否已缓存”优化)。适合对一致性敏感、写量不大但读量大的配置类、账户状态类数据。

五、Write-Behind:写性能最好,复杂度最高

Write-Behind 先写缓存并立即返回,再由后台任务批量、延迟地把变更刷入数据库。它把随机写合并为批量写,能显著降低数据库压力,适合计数、点赞、行为埋点等高写并发场景。但它引入了三个必须正视的问题:一是数据丢失窗口,Redis 宕机可能丢掉尚未落库的写;二是顺序与幂等,同一 key 的多次变更合并后要保证最终值正确;三是补偿,需要重试队列、死信记录和对账任务。没有这些配套,不建议在生产使用。

六、高并发下的取舍依据

模式一致性写性能实现复杂度典型场景
Cache-Aside最终一致(TTL 内)高低商品详情、用户资料
Read-Through最终一致高中读多写少、统一回源
Write-Through较强一致中中配置、账户状态
Write-Behind弱一致、可能丢写极高高计数、埋点、点赞
取舍口诀:读多写少用 Cache-Aside,回源要统一用 Read-Through,一致性优先用 Write-Through,写吞吐优先且能接受补偿成本才用 Write-Behind。同一系统内不必只用一种模式,可按数据域分别选择。
无论选哪种模式,都必须处理缓存穿透(空值缓存或布隆过滤器)、击穿(互斥重建或逻辑过期)和雪崩(TTL 加随机抖动)。模式解决的是读写路径问题,这三类问题解决的是失效瞬间的并发问题,二者不能互相替代。

落地时建议先按数据域画一张“读写路径表”:列出 key 前缀、读 QPS、写 QPS、可接受不一致时长、是否允许丢写,再对照上表选模式。例如订单状态可接受不一致 1 秒且不允许丢写,选 Cache-Aside + 延迟双删;文章阅读量允许丢少量计数,选 Write-Behind 并每 5 秒批量落库一次。把选择写成显式规则,比凭经验拍板更可靠。