点击追踪
周五下午,我收到本周第 10 封邮件,主题还是一样:
“我的链接到底被点了多少次?哪个渠道效果最好?我的推广预算花得值不值?”
这些问题我已经听了太多次。我看着自己的代码——创建短链接时只是存了个映射关系,重定向就是 302 跳转。完全没有记录任何访问信息!
落地思路
- 缺口在跳转链路上:302 直接返回,中间没有任何一步记录「谁在什么时候点了这个短码」。
- 要补的是三层信息:这次点击对应哪条链接、访问者从哪来、用什么设备来的。
- 约束是硬性的——记录动作不能出现在跳转的同步路径上,否则统计会拖垮跳转。
我知道,点击统计不能再拖了。营销用户需要知道他们的推广效果,我需要记录每次点击的详细信息。但我心里也很清楚——不能影响跳转速度。
第 4 版:异步统计链路
点击统计不能拖慢跳转,事件进入队列后再实时和离线消费。
跳转
事件
统计
存储
不阻塞跳转的点击追踪链路
redirect_cached跳转请求进入短链服务
优先读取 Redis / 本地缓存
直接拿到长 URL 和状态
点击事件进入队列,不阻塞跳转
低延迟完成跳转
我打开笔记本,开始列出需要追踪的数据:
基础信息
- 时间戳——什么时候点的
- 短链接——点了哪个链接
- 目标 URL——最终跳到哪里
用户信息
- IP 地址——知道大概位置
- User-Agent——解析出设备、浏览器、操作系统
- Referer——从哪里来的(搜索、社交媒体、直接访问)
- Accept-Language——用户语言偏好
渠道信息
- UTM 参数——营销人员最关心的来源标记
utm_source:来源(微信、微博、抖音)utm_medium:媒介(链接、二维码、Banner)utm_campaign:活动名称
地理位置
- 国家——IP 推算
- 城市——IP 推算
我写了数据收集函数:
落地思路
- 收集函数从请求对象里取基础信息:请求时间、路径里的短码、跳转的目标 URL。
- 环境信息从请求头里取:IP、User-Agent、Referer、Accept-Language。
- 渠道信息从查询串里取 UTM 参数,取不到就留空,不要伪造默认值。
- 收集函数只负责组装,不做存储决策;组装失败也不能影响跳转,采集异常直接跳过这次记录。
解析 User-Agent
User-Agent 是个复杂的字符串,我写了几个解析函数:
落地思路
- 解析分两步:先按 UA 字符串里出现的关键字判定设备类型(移动/桌面/平板),再判定浏览器和操作系统。
- 判定用包含匹配而不是完整版本号,统计只需要分类,不需要精确版本。
- 认不出来的一律归到 other,不要丢,也不要让异常 UA 抛错。
- 解析在收集阶段完成,原始 UA 保留在明细里,方便事后回溯判定错误。
数据收集好了,接下来就是存储。我最开始的想法很简单——直接写数据库:
数据设计要点
- 核心是在
click_logs里保存业务事实,而不是把规则散落在应用逻辑里。- 关键字段包括
id、short_code、long_url、ip、country、city、device、browser,它们决定了后续查询和管理能力。
落地思路
- 同步方案是:跳转请求内直接对
click_logs执行一次 INSERT,写完再返回 302。- 一次点击多一次写事务,写入耗时和锁等待全部叠加在用户的等待时间里。
- 跳转是高频写、统计是低价值写,两者放在同一条同步链路上,成本结构必然失衡。
- 兜底方向只有一个:把写入从跳转路径上摘出去。
我部署上线,开始监控性能。结果让我倒吸一口凉气:
之前:平均响应时间 20ms
现在:平均响应时间 100ms
跳转速度涨了 5 倍! 😱
我一看数据库监控,每次跳转都要执行一次 INSERT,数据库 QPS 瞬间飙到 1000+。这不行——用户体验会严重下降。
同步写入是死路。我需要异步。
我的新方案:缓冲区 + 批量写入 + 后台线程。
核心思路:
- 收集到点击数据后,不直接写数据库
- 放入内存缓冲区
- 后台线程定期批量写入数据库
- 跳转响应不等待写入完成
落地思路
- 点击数据先进内存缓冲区,不直接落库;跳转响应不等待写入完成。
- 后台线程按批取走:一批攒够或到时间上限就触发一次批量写入,把逐条 INSERT 合并成批量 INSERT。
- 批次写入失败时整批进重试队列,重试仍失败就落到本地文件等待补写,绝不能静默丢数据。
- 缓冲区要有上限:写满说明数据库跟不上,此时优先保住跳转,宁可丢计数也不能让请求排队等写入。
再次部署上线,监控结果:
平均响应时间:22ms ✅
数据库 QPS:从 1000+ 降到 50(批量写入)
完美!跳转速度恢复正常,数据库压力也大幅降低。
随着用户量增长,日点击量从 10 万涨到 100 万,再到 1000 万。内存缓冲区方案开始吃力——缓冲区太大占用内存,进程重启会丢失数据。
我引入了 Kafka:
用户点击
↓
[短链接服务] → [Kafka] → [消费者] → [数据库/ClickHouse]
↓
消息队列缓冲
峰值可抗 100K+/秒
落地思路
- Kafka 接管了内存缓冲区的职责:点击事件作为消息投递到队列,由消费者批量落库。
- 消费者按分区顺序消费,同一分区内有序,统计口径不会因为并发消费而乱。
- 消息保留一段时间,消费者宕机重启后可以重新拉取未处理的消息,比内存缓冲区更抗丢。
- 代价是多了一个需要运维的组件,所以按日点击量决定是否值得引入。
三种方案对比
| 方案 | 响应延迟 | 吞吐量 | 可靠性 | 复杂度 |
|---|---|---|---|---|
| 同步写 DB | 10-50ms | 1K/s | 高 | ⭐ |
| 异步批量写 | <1ms | 10K/s | 中 | ⭐⭐ |
| Kafka | <1ms | 100K+/s | 高 | ⭐⭐⭐ |
建议:日点击 < 10 万用异步批量写,> 100 万上 Kafka。
我重新审视了数据表设计,做了些优化:
数据表设计
数据设计要点
- 核心是在
click_logs里保存业务事实,而不是把规则散落在应用逻辑里。- 关键字段包括
id、short_code、ip、city_id、browser、os、referer、utm_source,它们决定了后续查询和管理能力。
数据量估算
每条记录约 200 字节
日访问量 50 万 = 每天 100MB = 每月 3GB
日访问量 1000 万 = 每天 2GB = 每月 60GB
数据存好了,用户要能查。我写了几个常用查询:
数据设计要点
- 统计查询的入口是短码和时间范围,靠
click_logs上的短码索引和创建时间索引支撑。- 查询目标是用短码快速定位目标链接的点击明细或统计结果,避免在跳转路径上做大范围扫描。
统计 API
落地思路
- 统计接口按短码和时间范围查询,命中的是
click_logs上的短码索引和创建时间索引,不走全表扫描。- 返回结果区分总量和去重量:总量来自计数,去重量来自独立访客的估算结构。
- 时间范围作为查询条件必须下推到索引上,否则范围一大就退化成扫表。
- 查询走独立的读路径,不能和写入共用同一条链路上的连接。
数据收集到什么程度?这是个问题。
我思考了很久,决定这条线:
我会收集
- ✅ IP(脱敏到前 3 段)
- ✅ 城市(不收集精确位置)
- ✅ 设备、浏览器、操作系统
- ✅ 来源渠道(UTM 参数)
我不会收集
- ❌ 完整 IP
- ❌ GPS 位置
- ❌ 个人身份信息
- ❌ Cookie 跟踪
IP 脱敏实现
落地思路
- 脱敏在写入前完成:取到 IP 后截掉最后一段,库里的 ip 字段只有前 3 段。
- 截断后的 IP 仍能定位到大致地域和网段,但无法回指到具体设备。
- 脱敏要在采集阶段就做,不能先存完整 IP 再定期清理——中间那一刻完整 IP 已经落在库里了。
- 需要按完整 IP 做的去重统计,用独立的哈希值代替,原始 IP 一律不留。
这样既能提供有价值的统计数据,又保护了用户隐私。这是我的底线。
点击追踪会影响重定向的速度吗?
会,如果同步写入的话。我改成异步队列后,响应时间从 100ms 降回 20ms。如果追踪系统暂时不可用(如 Kafka 挂了),应该怎么处理?
我的选择是放行——宁可丢数据,也不能影响用户体验。可以在本地文件缓存,等恢复后再补传。如何防止伪造点击数据(刷量)?
这是个难题。我做了几个防护:
- 限制同一 IP 的短时间点击频率
- 检测异常 User-Agent
- 识别代理 IP
- 统计异常模式(比如连续点击同一链接)
发现内容有问题?
告诉我们哪里需要改进。
