在海王出海或类似SCRM平台,处理重复点击的关键是把每一次“点击”变成一个能被识别的唯一事件,然后在服务端做短时幂等/判重。常见做法包括前端防抖与生成 click_id,后端用 Redis/数据库做快速判重(SETNX/唯一索引),结合 IP/UA/设备指纹与速率限制降低误判,并把去重窗口、日志与告警一起打通,既要防刷又要防误杀。

我先把问题拆成几块,像解释给朋友那样
想象一下,你给客户发了一个推广链接,或是在多渠道把同一个用户信息同步到平台,结果海王出海统计到好几条“相同”点击或事件。我们要做的,就是把“我到底以前处理过没”这个问题变成可以计算、快速回答的题目。下面一步步来讲清楚为什么会重复、有哪些判重思路、怎么在实操中落地、以及注意的坑。
为什么会出现重复点击(常见原因)
- 用户行为:双击、刷新、回退重试或网络卡顿导致客户端多次发送请求。
- 客户端实现问题:按钮没有防抖、事件绑定重复、SDK重试策略未考虑去重。
- 网络/中间件重试:代理、CDN 或 webhook 重试导致同一事件被多次投递。
- 聚合渠道重复:不同社交平台或账号同时上报同一用户行为,或第三方工具回传重复数据。
- 恶意刷量:脚本/机器人大量重复触发,或流量劫持导致重复点击。
去重的基本原理:把“相同”定义清楚
去重的第一步是定义“相同”的含义。是完全同一条事件(同一 click_id),还是同一用户在短时间内的多次动作?要不要跨设备去重?答案会决定实现方式。常见维度包括:
- 事件唯一 id(click_id、request_id)
- 用户标识(user_id、device_id、cookie)
- 行为时间窗口(10s、1min、1h)
- 来源渠道或消息 ID(平台原始 id,可做最终判定)
两类去重模型
- 严格幂等(event-level):以唯一事件 id 为准,发现相同 id 则直接丢弃或返回已处理结果。适合 webhook、回调、支付类场景。
- 概率去重(heuristic):用用户、IP、UA、时间窗口等组合规则判断相似事件并合并,适合点击/浏览等行为统计。
技术实现:一套可落地的步骤(前端 + 后端 + 中台)
下面给出从前到后的实操流程,按 Feynman 那种把复杂的东西拆成简单块来写。
前端(第一道防线)
- 按钮防抖/防连点:点击后禁用按钮或 300–1000ms 的防抖。
- 生成唯一 click_id:UUID v4 或基于时间戳+随机数的字符串,放在请求里(query/body/header)。
- 记录本地状态:把最近的 click_id 存 localStorage/sessionStorage,用于短期断线重连时复用。
后端(幂等与快速判重)
服务端是最可靠的判定点。常见做法:
- 请求必须带 click_id 或 X-Request-Id。若没有,则按 user+time 矩阵做后端生成并打标。
- 用 Redis 做快速判重(SETNX + EXPIRE),延迟极低,适合高并发。
- 对关键资源(比如写入数据库)使用唯一索引(唯一约束)作为最后防线,保证强一致。
示例:Redis 判重伪代码
下面是一个常见且推荐的原子操作思路(伪代码):
伪代码思路:先用 SETNX 尝试写入 click_id,成功则继续处理,失败则认为是重复并直接返回。
(这里写成自然语言伪代码,方便理解)
- key = “dedup:click:” + click_id
- if redis.SETNX(key, now) == 1 then redis.EXPIRE(key, ttl_seconds); 继续处理并记录日志
- else 返回重复响应或直接丢弃
如果要防止跨节点重复(并发写入)
可以把 SETNX 与 Lua 脚本结合,或者使用 Redis 的 SET key value NX PX milliseconds 原子命令,保证写入与过期的原子性。
落地的更多细节:设计好键与时窗
去重键的构成很重要,决定误杀率和准确率。常见组合:
- click_id(首选)
- user_id + event_type + time_window(比如 user:123|type:click|window:30s)
- channel_message_id(渠道原生 ID,例如某条社媒消息的 msg_id)
| 场景 | 建议去重键 | 建议时窗 |
| 广告点击(短时) | click_id 或 user+ad_id | 5–30 秒 |
| 表单提交(leads) | submission_id 或 user+form_hash | 30–300 秒 |
| 消息消费(webhook) | provider_msg_id(渠道原生唯一 id) | 24 小时或更长(按业务) |
扩展方案和优化
1) Bloom Filter(内存友好型)
在海量去重场景下,Redis 的内存压力会很大,可以先用布隆过滤器做第一层过滤(误判为存在,但不会漏判)。对大流量场景很有用,但不能替代可靠唯一约束。
2) 指纹/设备指纹
把 IP+UA+屏幕分辨率+时间片等做成“指纹”,用于概率合并重复行为。对抗刷量有帮助,但容易误伤真实用户(例如共享网络的场景)。
3) 速率限制与挑战验证
当同一 IP/指纹在短时间内触发大量事件时,先做速率限制(如 API Gateway 限流),严重时弹出挑战(验证码或更严格的校验)。
4) 消息队列+去重表
把事件统一先入 Kafka/RabbitMQ,再由消费端做去重判定并落库。好处是解耦、可回放、方便离线排查,但增加延迟与系统复杂度。
误杀(False Positive)与权衡
实际工程中,去重不是越严越好。过短的去重键会漏,太长或组合过宽又会把合法多次点击合并掉。几个降低误杀的技巧:
- 优先使用事件级唯一 id(click_id)而不是仅靠 IP/UA。
- 对不同事件类型选择不同的去重时窗。
- 保留原始日志,允许人工/离线复核和回滚误杀。
- 对重要事件做二次确认(例如订单支付),用幂等设计而非直接丢弃。
监控与测试——不能只靠“看着不变多”
去重上线后要有可观测性,建议至少监控这些指标:
- 原始事件量 vs 去重后事件量比率(重复率)
- 因去重拒绝的请求数(按 key 或规则分类)
- 服务器端响应延迟与 Redis 命中率
- 异常峰值(短时间重复率突增),配合告警
测试方面,可以写脚本模拟双击、网络重试、批量重复投递,确认去重行为和误判率。
进阶:当流量极大或渠道复杂时的策略
- 分层去重:先布隆过滤器再 Redis 最后数据库唯一索引。
- 分区/分表:按时间或渠道分区存储去重记录,避免单表/单 key 过热。
- 异步合并:对非关键统计事件,允许批量合并,降低实时判重压力。
- 机器学习:用行为模型识别刷量模式,对高疑似刷量行为采取更严格策略。
在海王出海场景下的实际建议(可按步骤操作)
- 检查前端埋点和 SDK:确保每次点击或转化有 click_id,按钮做防抖。
- 服务端强制要求 X-Request-Id 或 click_id,统一字段名,所有接入渠道遵循。
- 用 Redis SET key NX EX ttl 实现快速幂等判重,ttl 根据事件类型调整。
- 对 webhook 与第三方回调,优先使用渠道提供的原始 message_id 做去重键。
- 建立监控面板:重复率、拒绝数、误杀率;并对异常设置告警阈值。
- 对高级场景(高并发/跨渠道)考虑布隆过滤器 + 后端唯一索引的组合。
一个可直接用的小提示(工程化)
如果你在海王出海的接入端或后端还没有任何去重实现,先做这三件事,收益最大:
- 前端生成 click_id 并随每次请求上传
- 后端用 Redis 做 SETNX + EXPIRE 判重
- 关键写入(如用户表、订阅表)上唯一索引,作为兜底防线
嗯,写到这里我想到如果你的系统已经比较复杂,可以把现有埋点、后端中间件的调用流程截图抄下来(别发我图片就是说说细节),我可以帮你把判重逻辑写成具体的中间件伪代码,或者把 Redis/Lua 脚本、数据库 DDL 都给你。就这样,先做到能看得见效果的那几步,会比一次性做一个“大而全”的系统更稳妥。