【背景】 我做了 3 年数据工程,最近在一个读多写少、但写路径对一致性敏感的场景里反复纠结一件事:缓存和 DB 到底怎么保证「读到的不是脏数据」。我们这套是 MySQL 8.0 + Redis 7(Cluster,16 shard)+ Spring Boot,核心是「商品/库存」类的读接口,QPS 峰值 2 万,写 QPS 大概 1.5k,缓存命中目标 98%+。 问题在于:写路径每次都要保证「删缓存」这件事本身不出错,而读路径要扛住「删了缓存但 DB 还没提交完」那几百毫秒的窗口。这个窗口怎么关、关到什么程度,我们内部至今没有统一答案,每次上线前都要吵一轮。
【常见分歧 / 几种做法】 我把见过的方案按「一致性强度 → 复杂度」大致排了个序,欢迎大家对号入座: 1)Cache-Aside:先更新 DB,再删缓存(或先删再更,各有坑)。最朴素,但「删缓存失败」和「并发读写交叠」是两个经典漏点,很多人靠「延迟双删」(删一次→ sleep 500ms → 再删一次)去补那个窗口。 2)在 1)基础上加 binlog 兜底:DB 变更走 Canal/Debezium 抓 binlog,异步再打一次「删缓存」,把「删缓存失败」从主链路里剥离出去,主链路只做业务写。 3)更重的:写路径直接双写 DB + Redis,或者干脆用 Redis 当主、DB 当从做离线对账——把一致性责任从「删得对不对」转移到「同步得对不对」。 4)最极端的:关键数据不走缓存,或者走强一致的事务(比如 Redis 8 的 transaction 语义 / 用 PG 的 logical replication 做镜像),牺牲一点吞吐换「永不读到脏」。
【我的倾向与理由】 我倾向「主链路只做业务写 + binlog(CDC) 异步删缓存」,理由有三:
- 一致性窗口从「不可控的删失败」变成「可观测的同步延迟」:CDC 的 lag 是个数字,能告警、能 SLA;而「双删 sleep 500ms」是个拍脑袋的魔法数,500ms 够不够取决于 DB 那条写的 RT,而 DB 的 RT 本身就是抖的(连接池、GC、锁都会拉高它)。
- 它把「正确性」从「调用方记得删」变成「系统兜底删」,少一个分布式失败点。双删里那个「第一次删了、DB 写失败、第二次删又删了一个其实没变的数据」的竞态,CDC 方案天然没这个问题,因为它是跟着已提交的 binlog 走的。
- 代价是引入了一个「准强依赖」:CDC 链路挂了,缓存就会静默变脏,且脏多久取决于 lag 监控有没有兜住。所以我一般要求 CDC 的 lag P99 进 SLO,lag 超阈值直接降级到「读穿透到 DB」。
但我也不迷信 CDC:写 QPS 很低、且能接受「最多脏几百毫秒」的场景(比如运营后台的配置项),双删 + 延迟删完全够用,上 CDC 纯属过度设计——多一套组件、多一个 on-call 面,收益是负的。
【想听大家】 想听几类场景下的实际取舍,最好带点参数: 1)你们线上「删缓存失败」到底用没用延迟双删?那个 sleep 是拍脑袋还是有依据(比如对齐 P99 写 RT)? 2)上 CDC(Canal/Debezium/Flink CDC)的团队,lag 的 P99 一般压到多少?lag 超了你们降级到穿透 DB 还是直接报警人工? 3)有没有人真的踩过「双删还是脏了」的案例——具体是哪个竞态窗口漏的? 4)写 QPS 很低但一致性要求高的场景,你们会不会干脆「不缓存」或者「缓存但每次读都校验 version」? 抛个我特别想听的答案:你们团队有没有一份「什么情况用双删就够、什么情况必须上 CDC」的书面判据?还是每次都在 case-by-case 地吵?