如何设计数据库 CDC、数据同步与派生数据一致性
不要先背 Kafka 和 Exactly-once;先解释“主库正确,搜索和月报为什么错”
这一页只解决一个问题: 当数据库作为事实源,缓存、搜索、报表宽表这些派生数据怎样不漏、不重、不旧覆盖新,并且错了能重放、能对账、能修回来。
用户新增 88 元晚餐。主库事务已经提交,但首页缓存没变,搜索索引搜不到,月报宽表仍是旧金额。客服查主库说“数据在”,用户看页面说“数据没了”。两边都对,因为用户读到的是派生数据。
| 错误做法 | 为什么会失败 | 逼出的设计 |
| 应用写完主库后顺手写缓存、ES、消息 | 主库提交后进程崩溃,下游副作用丢失 | 事实源变化要有可追踪出口 |
| 每分钟扫表同步 | 延迟高、易漏更新和删除、扫表重 | 捕获提交后的真实变化 |
| 下游错了人工补 | 不知道漏哪段、重哪段、从哪里重放 | 位点、幂等、对账、重放 |
| 问题 | 例子 |
| 事实源是哪张表或哪个事件 | ledger_entries 是事实源,搜索索引不是事实源 |
| 下游需要行变更还是业务事件 | 搜索要行变化,预算提醒更适合 LedgerEntryCreated |
| 同步到哪个位点 | binlog / WAL 的 LSN、Kafka offset、consumer checkpoint |
| 重复事件怎么处理 | event_id、entry_id + version、处理记录表 |
| 乱序事件怎么处理 | 旧 version 不能覆盖新 version,删除墓碑不能被旧更新复活 |
| 错了怎么证明和修复 | 主库 vs 索引 / 宽表按用户、月份、entry_id 对账并重放 |
CDC 不是目的。最小可信模型要同时表达事实、事件、位点和消费结果。
outbox_events
- event_id
- aggregate_type
- aggregate_id
- event_type
- event_version
- payload
- source_lsn
- created_at
consumer_checkpoints
- consumer_name
- topic
- partition_id
- offset
- source_lsn
- updated_at
processed_events
- consumer_name
- event_id
- processed_at
吸收点: CDC 解决“变化怎么出来”;Outbox 解决“业务事件和主事务怎么同生共死”;幂等和对账解决“出来以后怎样不写错”。
| 边界 | 事故 | 设计动作 |
| 重复 | 同一事件重放 3 次,预算重复扣减 | 消费者按 event_id 或 entry_id + version 幂等 |
| 乱序 | 金额先改 108 后收到旧事件 98 | 旧版本不得覆盖新版本 |
| 删除 | 作废账目仍在搜索和月报里 | 删除 / 作废也要有事件和墓碑语义 |
| 全量 + 增量交界 | 新索引漏历史或重复历史 | 记录 snapshot_lsn,全量后从该位点追增量并对账 |
不要神化工具: Kafka 没积压、CDC 正在跑,只能证明链路在动,不能证明派生数据正确。
| 样本 | 通过标准 |
| 新增账目后杀掉消费者 | 恢复后从 checkpoint 继续,不漏事件 |
| 同一事件重放 3 次 | 搜索、报表、通知结果不重复 |
| 更新事件乱序到达 | 旧 version 不能覆盖新状态 |
| 作废一笔账 | 搜索、月报、预算都能撤销或排除 |
| 重建影子索引 | 历史全量 + 增量追平后和主库对账通过 |
| 按 20 分钟窗口重放 | 限速、幂等、差异归零,不制造第二次事故 |
总结: CDC 不是工具名,而是派生数据回到事实源的证据链。只要事实源、事件、位点、幂等、版本、删除、全量增量交界、对账和重放说不清,主库正确也挡不住用户看到错误结果。