打开追踪
打开追踪通常通过 Tracking Pixel 实现,例如在邮件中插入一个 1×1 像素图片:
https://t.example.com/o/{token}
用户打开邮件后,邮件客户端或邮箱代理请求这个地址,Tracking Gateway 收到请求即可完成一次基础事件采集。对于打开追踪来说,并不需要在 HTTP 请求过程中完成复杂的数据查询,可以先进行简单校验,例如 Token 中包含的用户 ID、邮件 ID、生成时间、随机数和校验和等,确认请求格式合法后立即返回一个透明图片,原始事件通过 Redis Streams 等消息队列异步处理。
打开追踪还有一个特点:一封邮件通常只有一个 Tracking Pixel,因此 Token 即使稍长,对邮件体积的影响也非常有限。同时,打开事件本身并不需要在请求时获取复杂的业务数据,因此没有必要为了极少量的读取操作建立一个高成本的实时查询链路。
退订追踪
退订链接和打开追踪比较接近,通常也是一封邮件只有一个退订链接,例如:
https://t.example.com/u/{token}
Tracking Gateway 收到请求后,首先进行 Token、用户 ID、邮件 ID、生成时间和签名等基础校验,然后记录一次 unsubscribe_event,再异步更新用户的退订状态。
退订请求通常不需要根据 Token 查询一个目标 URL,因此没有点击追踪中的实时跳转问题。真正需要关注的是退订状态更新的幂等性和一致性:同一个用户重复访问退订链接,不应该产生错误状态;即使用户连续点击多次,也应该最终保持一致的退订状态。
另外,退订属于用户明确的业务操作,因此与普通 Tracking Event 不同,建议优先保证状态变更可靠,再异步处理统计和分析数据。
由于一封邮件通常只有一个退订入口,Token 稍长也不会像点击链接一样大量增加邮件体积。因此这里可以携带相对完整的身份和校验信息,减少实时查询需求。
点击追踪
点击追踪的处理逻辑完全不同。
邮件中的点击链接通常类似:
https://t.example.com/c/{token}
用户点击后,Tracking Gateway 必须立即完成 Token 校验,并获得对应的原始 URL,然后返回 302 Redirect。因此,点击追踪属于一个典型的同步查询 + 快速跳转场景。
这里主要有两种设计方案。
方案一:短 Token + 服务端查询
链接只保存一个短 Token:
https://t.example.com/c/7Kx9Qa2
服务端通过 Token 获取:
message_id
recipient_id
link_id
target_url
这种方案 URL 短、邮件体积小,数据结构也容易扩展,是比较自然的设计。但问题也很明显:每次点击都需要查询 Token 对应的数据。
如果直接查询 PostgreSQL、MongoDB 等主数据库,大规模 ESP 场景下会产生明显的读取压力。因此实际实现不应该让数据库直接处于点击请求的热路径,可以采用:
Click Request
↓
Redis / KV
↓
target_url
↓
302
数据写入时预热 Redis,并设置合理 TTL;Redis 未命中时再回源持久化存储,查询成功后重新写入缓存。
这种方案的核心是:用缓存承担高频读取,而不是让数据库承担每一次点击。
方案二:自包含 Token
另一种方案是不保存完整的映射关系,而是将必要信息直接封装到 Token 中,例如:
version
message_id
recipient_id
link_id
target_url
expire_at
signature
经过压缩、编码和签名后生成最终链接:
https://t.example.com/c/{encoded_token}
Tracking Gateway 收到请求后可以直接验证签名、解析 Token、获取 target_url,整个过程不需要查询数据库。
这种方案最大的优势是几乎没有读取成本,特别适合超大规模点击追踪;但代价是 URL 会明显变长,而且邮件中每一个链接都会携带这些信息。一封邮件如果存在几十甚至上百个追踪链接,邮件体积会明显增加。
因此,自包含 Token 不应该把所有业务字段都塞进去,而应该尽量只保存跳转所必需的信息,并通过二进制编码、压缩和签名控制长度。
追踪服务的设计
综合来看,两种方案并不存在绝对的优劣,更适合根据 ESP 的规模和邮件类型选择。
| 方案 | URL 长度 | 实时查询 | 存储压力 | 适合场景 |
|---|---|---|---|---|
| 短 Token + Redis/KV | 短 | 有 | 中等 | 通用 ESP |
| 自包含 Token | 长 | 无 | 很低 | 超大规模点击追踪 |
对于一般 ESP,我更倾向于采用短 Token + Redis/KV + 异步事件流。点击请求只负责 Token 校验、读取目标 URL 和 302,点击日志通过 Redis Streams 等方式异步写入分析系统,避免把数据库写入放在请求链路中。
对于超大规模场景,则可以考虑自包含 Token,通过签名保证链接不可篡改,同时设置过期时间。这样可以把数据库读取成本转化为 URL 长度成本。
最终,Tracking Gateway 应该尽量保持简单:
Open
↓
基础校验
↓
记录事件
↓
异步处理
Unsubscribe
↓
基础校验
↓
更新退订状态
↓
异步记录事件
Click
↓
Token 校验
↓
获取 target_url
↓
302 Redirect
↓
异步记录 click_event
这里最重要的设计原则是:打开和退订追求低成本采集,点击追求低延迟跳转;不要为了统一数据模型,让三类请求都走同一种存储和查询路径。