同一笔转化被上报两次,是重复事件问题;点击在转化之后才到达,是迟到数据问题;转化原先归给广告甲、补齐数据后改归广告乙,是归因修正问题。把三者都交给“消息去重”处理,会让统计稳定地算错。

本篇只使用合成曝光、点击与转化,构建一个固定窗口的最后点击归因模型。没有真实广告用户数据,没有接入投放平台,也不计算应付账款。教学不变量是同一转化 ID 只进入一次业务总额、修正保持该笔总额守恒、窗口外点击不得获得归因。归因报表与资金账务的边界必须在模型之前确定。

事件时间、到达时间与业务时间不能混用

事件采用 event_id, kind, user_key, campaign_id, event_time, ingest_time;转化另带 conversion_id, amount_minor, currency。金额使用最小货币单位整数,本例 500 表示五百单位的教学金额,不暗示任何真实币种。身份关联键采用合成 u1,不讨论跨设备身份识别;若身份关联不可靠,归因本身应允许未知结果。

接入接口 POST /events 返回“已接受并持久保存”,不承诺归因结果立即最终化。查询 GET /conversions/{id}/attribution 返回 version, status, chosen_click, computed_at。同一事件 ID 不同内容必须拒绝或进入冲突区,不能凭相同 ID 丢掉一条纠错记录。更正应有新的事件 ID,引用被更正对象与版本。

本题归因规则是:转化发生前十个时间单位内,同一用户最后一次点击获归因;相同时刻按点击 ID 决定稳定次序。曝光只用于漏斗统计,不启用浏览归因,避免在案例中悄悄混入另一种窗口。十个单位是实验便于核算的教学窗口,实际产品需明确使用秒、小时或天,并定义时区及边界是否包含。

一条迟到点击如何改变报表

固定事件中 c1 在事件时间 10、到达时间 10;c2 在事件时间 18、到达时间 30;转化 x1 在时间 20 发生、金额 500,并重复上报一次。处理到达时间 20 时只知道 c1,因此产生暂定归因 x1→c1。到达时间 30 后补入 c2,重算得到 x1→c2。

修正不是新增第二笔 500 转化,而是给 c1 写 -500、给 c2 写 +500,二者相加为零。账本还应保存修正版本、原归因与新归因,便于重放和审计。若只更新最终聚合数字,报表看起来正确,却无法解释某个活动为何突然减少金额;如果直接追加 +500 给 c2,则全站金额被重复放大。

flowchart LR
    I[合成事件接入] -->|唯一 ID 与原始载荷| R[(不可变原始事件)]
    R -->|事件时间及关联键| A[窗口归因计算]
    A -->|暂定版本| V[(转化归因版本)]
    R -.->|迟到点击| A
    A -->|旧贡献撤回与新贡献加入| C[(修正明细)]
    C -->|可重放汇总| P[活动报表]
    V -->|查询解释| P

Apache Beam 关于水位线和迟到数据的文档解释事件时间处理里完成判断与迟到数据的关系。水位线表示对进度的估计,不是关于未来数据绝不会到达的自然定律。本题没有运行 Beam;仅用这一概念解释为什么报表需要“暂定”“已结算窗口”“超窗更正”不同状态。

若晚到数据超过允许修正期,不能随意把它丢进已关闭窗口并改写历史账单。统计可以在另一个修订版本展示新结果,财务则依据约定的对账和调整流程处理。此处没有税务、法务或渠道合同规则,文章不提供结算政策建议,只要求接口保留区分它们所需的信息。

容量与去重状态由窗口共同决定

教学规模每天一亿曝光、一千万点击、十万转化,总量 1.101 亿事件。平均约 110100000/86400≈1274.31 event/s,十倍峰值约 12743/s。每条压缩前 200 B,原始日量 22.02 GB;保留三十天三份约 1.9818 TB,索引、压缩和查询开销另计。入口带宽峰值约 2.549 MB/s,不能把记录每秒直接当成网络容量。

如果去重仅保留一天,而允许客户端离线七天后重新投递,同一转化第七天可能重新计数。去重键保留时间应覆盖重试与重放契约,或者将转化 ID 的唯一性放入更长期权威表。窗口状态还需要保留可参与归因的点击;把点击窗口从一天扩到七天,状态规模大体随保留时间增长,重算的候选数也会上升。

假设十万笔转化中 2% 因迟到点击修正,每次产生两条 100 B 明细,每天仅 0.4 MB;这不代表修正计算便宜,因为寻找候选可能扫描大量点击。应按用户与事件时间建立访问路径,范围查询后选择窗口内最大时间。热点合成用户若承载大量点击,既是性能异常,也可能是身份关联错误,需要单列告警,而不是扩大内存掩盖问题。

sequenceDiagram
    participant E as 事件流
    participant A as 归因器
    participant L as 修正账本
    E->>A: c1,事件时间 10
    E->>A: x1,事件时间 20,金额 500
    A->>L: v1:c1 加 500
    E->>A: 重复 x1
    A-->>E: 转化 ID 已处理,不增总额
    E->>A: 迟到 c2,事件时间 18
    A->>L: v2:c1 减 500,c2 加 500
    Note over A,L: 两条修正需要共同可核对,不制造第二笔转化

流式增量与批量重算如何配合

最小方案用原始日志加定时批处理,输出可解释的归因版本,适合允许分钟级或小时级延迟的报表。流式增量减少初步结果延迟,但需维护窗口状态、去重键和修正链。若业务暂不需要秒级报表,先做批量结果与原始明细对齐,比搭建复杂实时链路更容易验证。

两条路径同时存在时,批处理不是无条件真理:必须使用相同规则版本、身份快照、输入范围和金额单位。否则流式与离线差异可能只是窗口边界不同。重算任务输出差异清单,按 conversion_id 定位,不能只比较活动总额;两个错误贡献相互抵消时总额相同仍掩盖错误。

故障恢复从不可变原始事件重放,检查点只表示计算进度。若更新归因版本成功、修正明细写入失败,重启后可能缺失报表调整;工程实现应将两者置于同一事务或使用可核对的 Outbox。幂等键应包含转化 ID 与归因版本,避免重放同一修正多次。删除请求还需处理原始事件、身份映射与衍生结果的保留规则,不能仅从仪表盘隐藏一行。

运行 python3 examples/system-design/labs/E02/run.py,得到初始 c1、最终 c2、修正 -500/+500、去重后总额 500。负例不去重时得到 1000,窗口外的点击不能参与时间 100 的转化。版本、固定输入、命令与 JSON 结果在 examples/system-design/evidence/E02/。这是内存确定性模型,没有验证消息中间件、数据仓库事务或真实广告结算。

面试追问包括:转化 ID 由客户端恶意重复使用怎么办;点击先到却时间戳被回拨怎么办;迟到修正已经跨过财务结算期怎么办。第一项需要身份与载荷冲突校验,第二项需要事件时间可信度规则,第三项需要独立账务调整契约。把全部情况称为“最终一致”不能给出可执行方案。

[PATTERN] 去重决定一笔事件算几次,归因决定算给谁,修正决定历史如何变化;三者使用不同键与状态,并通过转化总额守恒核对。

实验附件与导航

可运行实验源码 · 本次原始结果

系列导读;容量和数据承诺分别沿用系列的方法,本文数字为独立教学假设。