海王出海过滤重复点击的核心做法是“前端防抖+后端幂等+短期去重存储”的组合:前端通过防抖、按钮禁用和一次性token减少绝大多数重复操作;后端以唯一事件ID或带签名的追踪链接做幂等校验,使用Redis(SETNX/EX)或数据库唯一索引在短期内去重并设置TTL;对异常高频或疑似机器人请求再用IP/UA/设备指纹与时间窗口规则补充判断,同时建立实时统计与告警来调整阈值和窗口,兼顾性能与误判率。

为什么要过滤重复点击?先把问题讲清楚
说清楚为什么要做去重,比直接给代码更重要。重复点击会带来假流量、虚高的点击率(CTR)、重复计费(如果和广告或付费事件相关),还会把客户、销售或业务数据污染,导致错误的营销决策。尤其是在SCRM聚合平台里,一个追踪链接可能被多渠道转发、社交平台回调可能重试、客户端网络不稳定会重发请求,这些都会产生“看起来像是重复”的事件。
常见重复来源
- 用户侧重复操作:用户误触、快速连点、网络卡顿后的重试。
- 客户端/SDK重试:短连接失败后自动重发事件。
- 社交平台或中间层回调重试:第三方回调在确认失败时会重复发送。
- 爬虫或机器人:批量访问造成大量相似请求。
- 并发写入:两条几乎同时到达的请求在持久化阶段重复入库。
过滤重复点击的总体思路(费曼式一步步拆解)
把系统拆成三层来理解和处理:用户界面(阻止无意义点击)、传输与接收(识别幂等事件)、存储与分析(短期去重与统计)。每一层都能拦下一部分重复,合起来效果最好。
第一层:前端拦截(立刻见效)
- 防抖(debounce)与节流(throttle):对短时间连续点击只处理一次。对于表单提交、购买按钮、分享按钮等尤其必要。
- 按钮一次性禁用:点击后立即禁用按钮并显示加载态,直到服务器响应或超时。
- 一次性Token(nonce):在页面/消息里生成一次性token,发送后不能重复使用。
- 提示与延时反馈:给用户明确反馈(如“已发送”),减少重复点击的主观动机。
第二层:传输与接收(做幂等)
前端能拦截很多,但网络重试与外部回调无法依赖前端。这里的关键是“幂等”——无论同一事件调用多少次,结果只应用一次。
- 唯一事件ID:每次触发行为带上全局唯一ID(GUID/UUID/或由平台签名生成的事件ID)。接收端以此ID做一次性处理。
- 链接签名与参数哈希:对于点击链路,构造带签名的追踪链接(如包含timestamp+hash),当参数和签名相同且在短期内出现多次即视为重复。
- 幂等消费者:消息队列/异步处理模块需要以事件ID为幂等键,确保重复消息不会导致重复执行。
第三层:短期去重存储(高效且可扩展)
用缓存做短期去重是工业界常用且高效的方式。具体做法和选择会影响延迟、成本与误判率。
- Redis SETNX/EX:到达事件时,尝试SETNX(key=event_id, value=1)并设置TTL。SETNX成功则处理并持久化,失败则认为重复并丢弃或合并。
- 数据库唯一索引:在业务表上对事件ID做唯一约束,插入失败视为重复。适合需要强一致性的场景,但并发性能与写入成本较高。
- 布隆过滤器/Hash结构:用于大规模高吞吐下的近似去重,误判(false positive)存在,需要谨慎使用。
- 时间窗口策略:设置合理的TTL,比如点击行为通常使用几十秒到几分钟,购买或重要事件可以更久。
具体实现步骤(工程化指南)
下面给出一个可操作的分步实施方案,既适合逐步落地,也能在生产中稳定运行。
步骤一:定义“重复”的边界
- 是按事件ID严格去重,还是按用户+链接+时间窗口模糊去重?
- 不同事件类型设置不同窗口:例如广告点击窗口30秒,注册事件窗口5分钟,订单支付窗口1小时。
- 为社交平台回调和客户端上报分别设计不同的策略。
步骤二:前端优先级策略
在Web与App SDK中实现通用逻辑:
- 按钮点击后立即禁用,并记录本地一次性token(localStorage/sessionStorage或内存)。
- 实现防抖:例如500ms防抖用于普通按钮,2s用于重要操作。
- 把事件ID或追踪签名附带到请求中。
步骤三:后端幂等校验(优先Redis)
示例伪代码(Redis SETNX思路):
1. event_id = 请求.event_id 或 生成(event参数哈希 + 时间窗口) 2. key = "dedupe:event:" + event_id 3. if Redis.SETNX(key, 1) == 1: 4. Redis.EXPIRE(key, TTL_seconds) 5. 处理事件(写数据库、触发后续逻辑) 6. else: 7. 记录重复日志或返回已处理响应
步骤四:持久化与唯一索引(防止并发写入)
在数据库中,给事件表添加唯一键,作为二次保险:
CREATE TABLE events ( event_id VARCHAR(64) PRIMARY KEY, user_id VARCHAR(64), type VARCHAR(32), payload JSON, created_at TIMESTAMP ); -- 插入时捕获唯一约束异常,视作重复
步骤五:高频/异常行为检测
- 对短时间内同一IP/设备大量点击做阈值封禁或限速。
- 对UA异常、没Referer、或明显爬虫特征的请求做挑战(验证码)或直接丢弃。
- 在Redis中为每个IP/设备维护计数器与滑动窗口(如Token Bucket或Leaky Bucket)。
不同场景的策略建议(实际可配置的阈值示例)
| 场景 | 推荐窗口TTL | 处理方式 |
| 普通点击(社交分享链接) | 30-60 秒 | 前端防抖 + Redis SETNX去重 |
| 表单提交 / 注册 | 1-5 分钟 | 前端禁用按钮 + 后端幂等ID + DB唯一索引 |
| 支付 / 关键业务事件 | 1 小时以上 | 幂等ID + 强一致性写入(事务或全局锁) |
| 第三方回调(Webhook) | 视第三方重试机制而定(通常30秒到5分钟) | 签名验证 + 幂等键 + 日志与告警 |
技术选型与成本权衡
下面拿几种常见实现比较一下,方便在不同规模下做抉择。
- Redis SETNX/EX:实现简单、延迟低,适合高并发短期去重。缺点是需要Redis容量与TTL管理。
- 数据库唯一约束:数据强一致、可靠,但并发高时会带来冲突和性能瓶颈。
- 布隆过滤器:节省内存、适合超大流量,但存在误判,不适合必须精确计数的业务。
- 消息队列幂等消费者:适合异步处理和事件驱动架构,缺点是增加系统复杂度和运维成本。
监控、告警与调优(不能忽略)
去重机制上线后不“放着不管”。你要监控这些关键指标:
- 重复率:重复请求与总请求的比值(按事件类型统计)。
- 命中率:Redis去重命中 vs 数据库去重失败次数。
- 误杀率估算:通过人工抽样判断多少合理请求被误判为重复。
- 系统负载:Redis命中率、DB冲突率、队列积压。
告警建议:
- 重复率异常升高(例如突然超过历史基线的5倍)。
- DB唯一约束异常快速上升(暗示某处未做幂等校验)。
- Redis内存使用或延迟上升。
调试与排查思路(遇到复杂重复问题怎么办)
遇到重复漏拦或误杀,按下面的顺序一步步排查:
- 查看原始日志:请求时间、event_id、来源、请求体是否完全一致。
- 确认前端是否正确传递event_id或token。
- 检查Redis键的TTL是否设置正确,或是否被意外删除/过期。
- 看是否有中间代理或CDN导致重放。
- 复现场景:用脚本模拟高并发或网络重试,看系统反应。
实用示例:为海王出海场景定制化建议
在SCRM平台中,你会面对多渠道、多语言、多端的数据,建议按如下方式落地:
- 在平台统一的追踪SDK里生成事件ID(例如:平台ID + 渠道ID + 时间戳 + 随机数),并对外提供签名链接生成接口。
- 在消息接入层配置Redis作为一级幂等缓存(TTL按事件类型配置),仅在SETNX成功时入队写DB;队列消费者可再做一次DB唯一约束保护。
- 对来自社交渠道的回调,先做签名校验、再检查event_id,最后做限速与风控筛查(IP/UA/设备指纹)。
- 对企业客户开放可配置的去重窗口和阈值,让不同客户按业务选择策略。
常见误区(避免踩坑)
- 把布隆过滤器当成精确去重工具:它可能误判,从而漏计真实事件。
- 只做前端保护、不做后端幂等:网络或第三方重发会造成严重重复。
- TTL设置过短或过长:过短会放过重试,过长会误杀正常重复但重要的后续操作。
- 忽视监控:一旦规则放在生产,就必须有数据支撑规则调整。
小结口吻的提示(边想边写的真诚话)
说实话,没什么“放之四海而皆准”的万能设置。稳妥的做法是先在非关键事件上试行去重策略,观察一段时间的重复率与误判,再逐步推广到关键业务。同时把去重逻辑做成可配置和可观测的组件,这样遇到特殊渠道或客户需求时可以灵活调整。技术上,Redis + 幂等ID 是效率和复杂度中最好平衡的方案,数据库唯一索引则是最后一道保险。对付爬虫或恶意刷量,还要和风控结合做更复杂的判定。好了,先写到这儿,具体到代码或配置的话,我们可以再把某个模块拆出来详细写——比如如何在海王出海的SDK里生成可信签名和event_id,或是如何在Kubernetes上部署高可用的Redis集群来支撑这个去重方案。