如何基于 binlog 做缓存一致性?
简化版
基于 binlog 的做法是订阅数据库已提交变更,经 CDC 转成带主键和位点的失效事件,再删除或刷新缓存。它把缓存维护从业务双写中解耦,但只能提供最终一致:必须处理事务提交顺序、重复投递、乱序、消费积压、位点恢复和误删新值。
详细版
典型链路为数据库提交事务 → binlog → CDC/Canal/Debezium → MQ → 缓存失效消费者。事件至少包含表、主键、操作类型、事务/日志位点和版本信息。多数场景选择删除而非把 binlog 行直接拼成缓存值,因为缓存可能是多表聚合、经过业务转换或包含权限语义。
消费者按事件 ID/位点幂等,删除不存在 Key 也应安全;同一业务键要保序或用数据库版本防旧事件晚到后删除/覆盖新缓存。消费失败进入重试和死信,积压期间可缩短 TTL或绕过高风险缓存。事务内多行变更要在 COMMIT 后才发布,不能把未提交中间状态暴露出去。
一致性窗口由数据库提交到缓存失效完成的端到端 lag 决定。监控位点差、最老事件年龄、失败/重试、缓存旧值抽检和重建 QPS;CDC 停止时要告警并有补位点、重放、全量校验能力。
完整版教学
一、为什么业务代码直接删缓存仍有缺口
Cache Aside 常在数据库事务提交后删除缓存。若数据库提交成功而进程在删缓存前崩溃,旧缓存会保留到 TTL;若把删除放在提交前,事务最终回滚时又可能把正确缓存无谓清掉,并在并发下重新写入旧值。
binlog 是数据库已提交事实的变更日志。订阅它可以让任何成功提交最终产生缓存失效动作,即使业务进程已经退出。它解决的是提交与通知之间的可靠衔接,但没有让数据库和缓存变成原子事务。
记忆钩子:binlog 方案的价值是“以数据库提交事实驱动失效”,不是承诺数据库与缓存瞬时一致。
二、完整链路和事务边界
CDC 连接数据库读取 binlog,解析行变更,并在事务提交后输出事件。下游将事件写入 MQ,消费者根据表与主键映射到缓存 Key,再执行删除或刷新。CDC 必须保存日志位点,重启后从已确认位置继续。
DB transaction COMMIT
-> binlog(position/GTID)
-> CDC 按事务组装
-> MQ(eventId, table, pk, op, version)
-> cache invalidator
-> DEL/UNLINK key
一个事务修改订单和订单项多行时,CDC 不应在看到第一行就对外宣布完整业务事件;至少要尊重 COMMIT 边界。跨表聚合缓存还需要规则把多行变更归到同一聚合 Key。
三、为什么通常选择删除而非更新
缓存值常不是某一行的镜像,可能组合商品、库存、促销和权限,也可能经过序列化与字段过滤。直接用 binlog row 更新容易漏掉关联表和业务逻辑,导致“看起来及时但值不完整”。删除后下一次读取从权威路径重建,规则更集中。
| 策略 | 优点 | 风险与适用条件 |
|---|---|---|
| 删除缓存 | 简单、幂等、由读路径重建 | 首次读增加回源,需防击穿 |
| CDC 直接更新 | 减少冷 miss | 仅适合单表镜像且转换逻辑稳定 |
| 标记版本失效 | 可拒绝旧值 | 需要缓存与请求携带版本 |
| 主动刷新热点 | 降低热点回源 | 需 singleflight/互斥和容量控制 |
删除后若热点同时回源,要配合请求合并、逻辑过期或异步预热。
四、重复和乱序如何处理
MQ 与 CDC 常使用至少一次语义,同一事件可能重复;DEL 本身通常幂等,但若消费者执行“读取数据库并 SET”,重复与乱序可能把旧版本写回。事件应携带数据库版本、提交序号或可比较位点,刷新时使用版本条件。
同一 Key 的事件应路由到同一 MQ 分区保持顺序。拓扑切换、重放或多表事件仍可能乱序,因此缓存值可包含 dataVersion;准备写 v18 时若当前已是 v19,应丢弃 v18。
事件 v18 延迟到达 -> 当前缓存 v19
DELETE 策略:若无法条件删除,删除 v19 会造成额外 miss,但不会长期旧
UPDATE 策略:必须拒绝 v18 覆盖 v19,否则发生状态倒退
若删除与重建并发,需防“旧查询晚写回”:重建前后检查数据库版本,或使用版本化 Key。
五、带数字理解一致性窗口
假设数据库提交后 CDC P99 延迟 200ms、MQ 排队 P99 300ms、消费者处理 P99 100ms,则缓存失效 P99 约为 600ms,尚未计算极端重试。业务若承诺 100ms 内读新值,这套异步链路不满足要求,需要写后读主、同步删缓存或版本校验补强。
CDC 故障 10 分钟、写入速率 5000 事件/秒,会积压 300 万事件。恢复后消费者 8000/s、持续新增 5000/s,净消化 3000/s,清空至少需 1000 秒,约 16.7 分钟;“组件恢复”不等于缓存一致性恢复。
consistency_lag = now - source_commit_time
recovery_time = backlog / (consume_rate - produce_rate)
六、CDC 故障与位点恢复
CDC 要持久化已安全发布的 GTID/LSN/position,并处理数据库日志保留期。如果故障超过 binlog 保留时间,不能从旧位点续读,需要从快照重建基线,再接增量。位点推进必须与下游发布确认协调,宁可重复也不能跳过。
表结构变更也会影响解析。DDL、字段重命名和类型变化需要 schema history 与兼容测试,不能让消费者默默按旧字段解释。敏感字段不应无条件进入 MQ,事件只携带失效所需的最小信息。
跨库分片没有天然全局顺序;通常只要求同一业务键有序,跨键并行。若业务要求跨库原子视图,单纯 CDC 删除缓存无法提供。
七、监控、校验与演练
监控从源到端:数据库当前位点与 CDC 位点、MQ lag、最老未处理事件、每类表事件量、删除失败、重试/死信和缓存重建压力。只看消费者在线无法发现位点卡住。
定期抽样比较数据库版本与缓存版本,或对关键 Key 做读旁路校验。发现大范围失效遗漏时,可按表/业务前缀批量失效,但必须限速,避免清缓存雪崩。演练 CDC 中断、MQ 重复、乱序、日志过期和 schema 变更。
cdc_source_lag_seconds: 0.18
mq_oldest_event_seconds: 0.42
cache_invalidation_failures: 3
dead_letter_events: 0
version_mismatch_sample_rate: 0.002%
八、常见误区与追问
- 误区:订阅 binlog 后缓存与数据库就是强一致。 事件传输和消费有延迟,提供的是可监控的最终一致。
- 误区:CDC 直接拿行数据更新所有缓存最及时。 聚合缓存可能依赖多表和业务转换,直接更新容易生成不完整值。
- 误区:删除缓存天然没有乱序问题。 旧删除事件可能删掉刚重建的新值,虽通常只造成 miss,仍会引发回源压力。
- 误区:消费者重启后从最新位置开始即可。 跳过故障期间事件会永久保留旧缓存,必须从已确认位点续读。
- 追问:binlog 被清理了怎么办? 从一致性快照重建缓存/CDC 基线,再从快照对应位点追增量。
- 追问:如何防止旧查询回填旧值? 使用数据库版本、版本化 Key、条件写或回填前后校验版本。
- 追问:业务还需要主动删缓存吗? 高一致性路径可同步删以缩短窗口,CDC 作为可靠兜底,但要接受重复并保持幂等。
九、加强记忆
按“提交事实、可靠传递、幂等失效、版本防倒退、位点可恢复”理解 binlog 缓存一致性。数据库 COMMIT 是源头,CDC 只在事务提交后发事件;通常删缓存而非重建复杂聚合;用同 Key 有序、版本与重放处理重复乱序,并用端到端 lag 和抽样对账证明最终会收敛。