3. 事件数据模型设计:用户事件的定义与分类、事件属性设计原则、事件时间与处理时间、事件ID与用户ID的关联
数据模型是实时归因的根基。模型没设计好,后面所有分析都是空中楼阁。我见过太多团队,一上来就急着写 Flink 作业,结果事件字段对不上、时间戳乱飞、用户 ID 串号——最后全得重来。
这一章,我们就来聊聊怎么把事件数据模型搭扎实。说白了,就是回答四个问题:事件是什么?属性怎么定?时间怎么管?人和事怎么关联?
核心原则:事件模型的设计,要以「分析场景」为驱动,而不是以「采集成本」为驱动。宁可多记一个字段,也不要事后补数据。
3.1 用户事件的定义与分类
先定义清楚:什么是用户事件?
我的定义很简单——用户在应用内完成的一个有业务意义的原子操作。比如「点击购买按钮」「提交订单」「播放视频」「分享给好友」。这些是事件。而「页面停留 3 秒」「滑动屏幕 2 次」这类,通常算作状态或指标,不适合作为独立事件。
事件分类,我习惯分成三大类:
| 事件类型 | 说明 | 典型例子 |
|---|---|---|
| 行为事件 | 用户主动触发的操作 | 点击、滑动、提交、播放、收藏 |
| 状态事件 | 系统或用户状态的变化 | 登录、登出、进入页面、离开页面 |
| 业务事件 | 与核心业务流程强相关 | 下单、支付成功、退款、注册完成 |
为什么要分这么细?因为不同事件在归因中的权重不一样。行为事件是归因的主体,状态事件用来做上下文,业务事件是最终转化的目标。我在项目中遇到过,有人把所有事件混在一个大表里,结果做路径分析时,状态事件和业务事件搅在一起,根本算不清转化率。
我的习惯:每个事件都带一个 event_type 字段,取值就是上面三类。这样在过滤和聚合时,可以快速区分。
3.2 事件属性设计原则
事件属性,就是描述这个事件的「细节」。比如「点击购买按钮」这个事件,我们需要知道:点击的是哪个商品?价格多少?从哪个页面跳过来的?用户用的什么设备?
属性设计,我总结了四个原则:
- 原子性:每个属性只表达一个含义。别把「城市+设备型号」塞到一个字段里。
- 可枚举性:能用枚举值就别用自由文本。比如
device_type用ios/android/web,而不是用户自己填。 - 可扩展性:预留扩展字段。我一般会加一个
properties_ext的 JSON 字段,放那些不确定的属性。 - 业务相关性:只记和分析相关的属性。别把 CPU 温度、内存占用这种技术指标塞进来。
举个例子,一个标准的「商品点击」事件属性设计:
{
"event_name": "product_click",
"event_type": "behavior",
"properties": {
"product_id": "sku_123456",
"product_name": "无线蓝牙耳机",
"category": "数码/音频",
"price": 299.00,
"source_page": "homepage",
"source_module": "recommend",
"device_type": "ios",
"os_version": "16.4"
}
}
嗯,这里要注意:price 字段我建议用浮点数,但如果你做实时聚合,最好转成整数(单位分),避免精度问题。我曾经因为这个踩过坑,实时计算里浮点数累加,结果对不上账。
3.3 事件时间与处理时间
这是实时计算里最容易搞混的概念。我见过不少新人,把这两个时间当成一回事,结果数据延迟一波动,归因全乱套。
简单说:
- 事件时间(Event Time):用户实际发生操作的时间。由客户端 SDK 生成,通常就是
timestamp。 - 处理时间(Processing Time):服务端接收到事件的时间。由 Flink 或 Kafka 等系统打上的时间戳。
为什么必须区分?因为网络延迟、客户端离线、服务端积压,都会导致处理时间远晚于事件时间。如果你用处理时间做归因,那凌晨 2 点的用户操作,可能被归到早上 8 点的活动上。
避坑指南:我曾经接手过一个项目,他们用处理时间做 30 分钟转化窗口。结果用户晚上 10 点点击广告,第二天早上 8 点才被服务端收到,转化窗口早就关了。最后归因率不到 30%。
我的建议是:所有归因逻辑,必须基于事件时间。处理时间只用来做监控和延迟告警。
在 Flink 中,设置事件时间的标准做法:
DataStream<UserEvent> eventStream = ...
.assignTimestampsAndWatermarks(
WatermarkStrategy.<UserEvent>forBoundedOutOfOrderness(Duration.ofSeconds(10))
.withTimestampAssigner((event, timestamp) -> event.getEventTime())
);
这里 forBoundedOutOfOrderness 设了 10 秒的乱序容忍度。为什么是 10 秒?不是拍脑袋定的。我一般会根据历史数据,统计 99 分位的延迟时间,然后取整。比如 99% 的事件在 8 秒内到达,我就设 10 秒。
3.4 事件ID与用户ID的关联
事件 ID 和用户 ID 的关联,是归因分析的基石。没有这个关联,你根本不知道「谁」做了「什么事」。
先说事件 ID。每个事件必须有一个全局唯一的 ID。我习惯用 UUID 或者 雪花算法 生成。为什么?因为实时计算中,事件可能被重复消费,唯一 ID 可以用来做去重。
用户 ID 就复杂一些。一个用户可能有多个 ID:
- 设备 ID:IDFA、OAID、IMEI(已限制)
- 登录 ID:手机号、邮箱、用户名
- 匿名 ID:Cookie、Session ID
我的做法是:统一用户 ID 体系。在客户端 SDK 里,优先取登录 ID,如果没有,就用设备 ID 或匿名 ID。然后在服务端维护一个 user_mapping 表,把各种 ID 映射到一个全局的 user_id 上。
事件和用户的关联,最终落地到数据模型上就是:
{
"event_id": "uuid_xxxxx",
"user_id": "global_user_123",
"device_id": "idfa_yyyyy",
"event_name": "purchase",
"event_time": 1712345678000,
"properties": { ... }
}
这里 user_id 是全局统一的,device_id 只是辅助字段。归因分析时,全部基于 user_id 来做。
我的习惯:在实时流中,我会用 user_id 做 keyBy,保证同一个用户的事件进入同一个算子。这样归因状态可以本地维护,不用跨节点通信。
3.5 本章知识体系总览
下面这张图,把事件数据模型的核心要素串起来了。你可以把它当作设计时的检查清单。
这张图把事件数据模型的四个核心要素串在了一起。你设计模型时,可以拿它当 checklist:事件分类清楚吗?属性满足四个原则吗?时间用对了没有?ID 关联打通了吗?