1. 缓存数据结构设计:事件模型、存储字段、序列化格式
埋点数据在设计缓存层之前,必须首先定义统一的事件模型与存储结构。数据结构直接影响序列化效率、存储空间、查询能力和容错恢复。本章围绕三个核心层次展开:事件模型(Event Model)定义业务语义,存储字段(Storage Fields)定义持久化视图,序列化格式(Serialization Format)定义字节流协定。
1.1 事件模型
一个最小完备的事件模型应包含以下五类信息:
- 标识层:事件唯一 ID(UUID / Snowflake)、应用版本、用户标识(ID / 匿名 ID)。
- 时间层:客户端时间戳(毫秒)、采集时间戳、事件产生序号。
- 类型层:事件分类(click / view / purchase / applog)、业务子类型。
- 属性层:键值对(string / number / bool / nested object),支持 JSON Schema 校验。
- 上下文层:设备信息、屏幕分辨率、网络状态、位置等预定义字段。
// 伪代码结构
Event {
eventId: string; // 全局唯一
eventType: string; // "page_view"
timestamp: number; // 客户端产生时的 unix ms
userId: string | null;
anonymousId: string;
properties: Record<string, any>;
context: {
deviceModel: string;
osVersion: string;
networkType: "wifi" | "4g" | "5g";
screenWidth: number;
screenHeight: number;
};
}
设计要点:属性层尽量使用扁平化键值对避免深层嵌套,以便后续在序列化前进行压缩。上下文层建议预定义固定字段集合以提升缓存规整度。
1.2 缓存存储字段
缓存层不直接存储原始事件对象,而是将事件模型与运维状态合并为一个 存储记录(Storage Record)。每个记录包含以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| record_id | UUID | 缓存记录主键,与事件 ID 1:1 对应 |
| event_type | string | 冗余存储,用于按类型过滤未上报事件 |
| client_ts | int64 | 客户端时间戳,排序与去重依据 |
| payload | bytes | 序列化后的事件二进制数据(见 1.3) |
| status | int8 | 0=pending, 1=in‑flight, 2=failed, 3=success |
| retry_count | int8 | 当前重试次数,上限 5 次 |
| next_retry_ts | int64 | 下次允许上报的时间戳(指数退避) |
| created_at | int64 | 记录写入缓存的时间 |
-- SQLite 表定义示例
CREATE TABLE IF NOT EXISTS event_cache (
record_id TEXT PRIMARY KEY,
event_type TEXT NOT NULL,
client_ts INTEGER NOT NULL,
payload BLOB NOT NULL,
status INTEGER DEFAULT 0,
retry_count INTEGER DEFAULT 0,
next_retry_ts INTEGER DEFAULT 0,
created_at INTEGER DEFAULT (strftime('%s','now') * 1000)
);
CREATE INDEX idx_status ON event_cache(status);
CREATE INDEX idx_next_retry ON event_cache(next_retry_ts);
1.3 序列化格式
缓存中的 payload 字段存储序列化后的事件对象。推荐使用 二进制 JSON(BSON / MessagePack / FlatBuffers) 或 压缩后的 UTF‑8 JSON。以下为使用 gzip + JSON 的方案:
// 序列化流程(伪代码)
function serialize(event: Event): Uint8Array {
const json = JSON.stringify(event); // 编码为 UTF‑8
const compressed = pako.gzip(json, { level: 6 }); // 压缩级别 6
return compressed;
}
// 反序列化
function deserialize(bytes: Uint8Array): Event {
const decompressed = pako.ungzip(bytes);
return JSON.parse(new TextDecoder().decode(decompressed));
}
推荐实践:生产环境中建议在序列化前对 properties 进行 字段排序(按 key 字典序),使相同事件的字节流完全一致,利于后续去重与差异压缩。
1.4 缓存数据结构关系图
1.5 常见错误与避免建议
- 直接存储原始 JSON 字符串:无压缩会浪费 40%~60% IO 带宽,尤其在高频事件场景下影响电池与磁盘寿命。
- 漏写 status 索引:导致每次上报遍历全表,客户端卡顿。必须为 status / next_retry_ts 建立复合索引。
- 序列化时修改字段类型:例如将 number 转成 string 存入属性,引起上游解析报错。要保持 schema 严格一致。
- 忽略 payload 最大尺寸:单条事件 payload 不应超过 64KB(含头部),否则应分包或丢弃。
性能拐点:当缓存记录超过 5000 条时,全表扫描 status=0 的查询耗时可能超过 100ms。建议在插入时同时维护一个 待上报计数缓冲(例如使用内存队列 + 定时批量更新状态),避免每次上报都读数据库。
1.6 调试与验证
推荐在开发阶段开启 payload 明文存储开关,便于直接查看事件内容。上生产前必须关闭明文存储,仅保留序列化后的 BLOB。
// 调试模式配置(仅在 DEBUG=true 时启用)
config: {
payloadFormat: Uint8Array | "rawJson",
debugStore: process.env.NODE_ENV === "development" ? "rawJson" : "binary"
}
使用 SQLite 原生工具验证缓存记录完整性:
# 进入数据库,查询前 5 条待上报记录
sqlite3 ~/.appdata/event_cache.db
SELECT record_id, event_type, length(payload), status FROM event_cache
WHERE status = 0
ORDER BY client_ts ASC
LIMIT 5;
通过比较 length(payload) 与预期压缩比值(通常 3:1~5:1),可快速发现序列化异常或压缩 bug。