统计平台的数据问题,一半出在“同步”:要么同步慢了,要么同步漏了,要么同步错了
统计平台最典型的数据链路是:
1 | 业务库 → 同步 → 宽表/搜索索引 → 指标查询 → 看板 |
中间这一步“同步”,早期通常靠定时任务轮询:
1 | SELECT * FROM `order` WHERE update_time > 上次时间; |
轮询能跑,但有三个绕不开的问题:
- 轮询间隔决定延迟,想快就得多查;
- 查出来的数据不一定完整,订单的关联表可能还没更新;
- 删除、状态变更、关联表变化,很难用一条 SQL 兜住。
cdc-go 解决的正是这一段:把 MySQL 的行级变更实时读出来,转成统一事件,投递到消息队列,由下游消费者决定怎么用。
本文不重复它的安装步骤,而是聊一个更实际的问题:在数据统计平台里,cdc-go 应该怎么用。
一、cdc-go 提供的是什么
cdc-go 是一个 Go 编写的轻量级 MySQL CDC 服务,Apache 2.0 开源。它的职责很单一:
1 | MySQL binlog → cdc-go → Kafka / Redis / RabbitMQ / RocketMQ / Pulsar / stdout |
它读取 ROW 格式的 binlog,把每一次行变更转换成一个统一的 JSON 事件,再发送到配置的输出端。位点保存在文件或 Redis 里,重启后从上次的位置继续,而不是从头重放。
几个和统计平台直接相关的特性:
- 实时:按行变更捕获,不需要等定时任务;
- 统一格式:insert / update / delete 都是同一种结构;
- 位点续传:重启不丢进度;
- 过滤表:用正则只订阅需要的库表;
- 背压:下游慢的时候,读 binlog 的速度会跟着慢,而不是丢事件。
二、事件长什么样
event.json
1 | { |
字段含义:
| 字段 | 说明 |
|---|---|
time | cdc-go 捕获事件的时间 |
pos | 对应的 binlog 位点 |
row.time | binlog 事件时间,Unix 秒 |
row.database / row.table | 来源库表 |
row.type | insert、update、delete |
row.data | 变更后的整行;删除时是被删的行 |
row.old | 变更前的整行(只有 update 有) |
注意 old 是整行,不是只包含变化的列。这个设计对下游很重要:它意味着你可以从事件里判断“哪个字段变了”,但不要指望它只给变化的字段。
三、在统计平台里的位置
把 cdc-go 放进统计平台的链路,大概是这样的:
1 | 业务 MySQL |
这里有一个关键设计:事件只是“哪一行变了”的提示,文档内容以数据库当前状态为准。
为什么不直接用事件里的 data 拼文档?
因为统计用的宽文档通常跨很多张表:订单、商品、支付、退款、评价、渠道……当退款表变化时,事件里只有退款这一行,拼不出完整订单。回库读当前状态,才能保证:
- 重复投递、乱序投递、补数重放,最终写入的都是当前真实状态;
- 模型加字段时,不用改消费逻辑;
- 出问题可以随时按主键重跑。
代价是每条变更要查一次库,对数据库有压力;这个取舍在统计场景是值得的——统计要的是“对”,不是“快一秒”。
四、消费侧怎么设计
cdc-go 只负责投递事件,消费侧要自己实现。统计平台的消费者,通常需要处理三类运行方式:
| 运行方式 | 触发 | 用途 |
|---|---|---|
| 实时消费 | Kafka 事件 | 正常增量同步 |
| 按主键重跑 | 手动 / 补偿 | 修某一条数据 |
| 按时间区间重跑 | 手动 / 定时 | 补历史、兜底漏数 |
4.1 主表和子表
一个订单的宽文档,可能依赖十几张表。消费侧需要声明:
- 主表:订单表,主键是
id; - 子表:退款、评价、商品明细等,通过
order_id指向主表。
这样,无论变更来自主表还是子表,消费者都能算出“应该重写哪一条订单文档”。
4.2 幂等写入
事件可能重复(重投、补偿重叠),所以写入必须幂等:
- 用“合并更新 + 冲突重试”写入文档;
- 删除不物理删除,而是打一个“排除”标记,保证历史报表可重算;
- 所有字段显式写入,空值也要写空,避免旧值残留。
4.3 补数
实时链路会漏事件:消费端重启、消息积压、下游限流。所以还需要一层兜底:
- 按时间区间分页取主键,逐页重跑;
- 时间列不能只看创建时间,支付、完成、退款时间都要纳入;
- 补数和实时共用同一套拼装逻辑,保证结果一致。
五、实时与离线怎么配合
统计平台通常同时存在实时和离线两条链路:
1 | 实时:binlog → cdc-go → 消费者 → 宽文档 → 实时看板 |
分工建议:
- 实时负责“快”:看趋势、看异常、辅助决策;
- 离线负责“准”:结算、考核、对外承诺;
- 补数负责“兜底”:实时漏了、错了,按区间重跑。
三者共用同一份指标定义和同一套拼装逻辑,差异才可解释。
六、配置与部署要点
cdc-go 用一份 YAML 配置,分三段:位点存储、数据源、输出端。
config.yml
1 | store: |
几个容易踩的部署点:
- server_id 必须唯一:不能和 MySQL 副本、其他 CDC 实例冲突;
- 位点要持久化:容器环境用 Redis,不要用临时文件系统;
- 只订阅需要的表:用 include/exclude 正则收窄范围,减少无关消息;
- 密码走环境变量:配置支持
${VAR}和${VAR:-fallback},不要把密码写死; - 先看 stdout:本地验证时把 producer 设为 stdout,能直接看到事件结构。
七、常见坑
- 用事件里的 data 直接拼文档:跨表字段拼不出来,必须回库读;
- 按
row.time当业务时间:它是 binlog 事件时间,不是订单创建/支付时间; - 主键类型假设错误:事件里的主键可能是 int64、string 或 bytes,要按任务已知类型读取;
- 重复事件没去重:重投、补偿重叠都会造成重复写入;
- 删除物理删文档:历史报表重算会变,应该打排除标记;
- 只按创建时间补数:支付、退款、完成时间命中的单会漏;
- 位点存本地文件:容器重启后位点丢失,从头重放;
- server_id 冲突:多个 CDC 实例抢同一个 id,连接会被踢;
- 大事务:一次性产生大量事件,消费端要能扛住积压;
- DDL 变更:加字段后事件结构变化,消费端要兼容。
八、监控什么
cdc-go 把变更搬出来之后,统计平台还需要监控整条链路:
- binlog 延迟:事件时间到当前时间的差距;
- 消息积压:Kafka lag 是否持续增长;
- 消费失败率:任务报错的比例;
- 数据延迟:事件产生到文档可查的时间;
- 补数进度:区间补数跑到哪、还剩多少。
只监控 cdc-go 本身不够,要监控“数据从产生到可查”的端到端延迟。
九、小结
cdc-go 解决的是“变更怎么搬出来”,统计平台要解决的是“搬出来之后怎么用”。
用一句话概括最佳实践:
事件只做提示,文档以库为准;实时负责快,补数负责全,指标负责准。
把这三件事分开,统计平台的数据链路才能既快又稳。
cdc-go 项目地址:https://github.com/chihqiang/cdc-go