如何检测并拦截同一时间发往服务器同URL的重复并发请求
同URL并发重复请求拦截落地方案
先明确两类需要拦截的异常场景
不要上来就全拦同URL请求,先区分场景避免误伤正常业务:
- 第一类:前一个同参数请求还在等待响应,后续相同请求就已经发出来了,多是客户端逻辑bug导致重复触发调用,这类可以直接复用第一个请求的返回结果,不用重复发请求到服务端。
- 第二类:客户端出现无限循环bug,哪怕前一个请求已经返回结果,依然会高频重复发起相同请求,这类需要靠限流熔断机制拦截,避免打垮服务端。
判定重复请求的核心维度不能只看URL,必须同时带上请求方法、用户身份标识、请求核心参数哈希三个维度,避免把不同用户、不同参数的正常同接口请求误拦截。
分层实现方案
客户端侧前置拦截(第一道防线,拦截80%以上异常流量)
- 并发请求去重缓存:维护一个正在执行的请求映射表,key是前面说的三个维度生成的唯一标识,value是对应请求的Promise实例。相同key的新请求进来时,直接返回已经在执行的Promise,不发起新的网络调用;等第一个请求返回(成功/失败)后,删除映射表中对应key,后续新的同类型请求可以正常发起。
简单JS实现参考:
import hashCode from 'light-hash-lib'; // 任意轻量哈希函数即可 const pendingRequestMap = new Map(); // 生成请求唯一key function getRequestKey(config) { const { uid, method, url, params, data } = config; return `${uid}_${method}_${url}_${hashCode(JSON.stringify(params) + JSON.stringify(data))}`; } // 给axios实例加拦截器 function addDuplicateFilter(axiosInstance) { axiosInstance.interceptors.request.use(config => { const key = getRequestKey(config); // 命中正在处理的重复请求,直接返回已有Promise if (pendingRequestMap.has(key)) { return pendingRequestMap.get(key); } // 正常发起请求,存入缓存 const reqPromise = axiosInstance.request(config); pendingRequestMap.set(key, reqPromise); // 请求结束后清缓存 reqPromise.finally(() => pendingRequestMap.delete(key)); return reqPromise; }); }
- 高频异常熔断:给每个接口配置滑动窗口阈值,比如单用户1秒内同接口同参数请求超过5次,直接触发10秒的熔断,熔断期间的同类型请求直接拦截在客户端,同时上报错误日志,方便排查循环bug。
服务端侧兜底防护(第二道防线,防客户端绕过前端防护的场景)
- 接口粒度限流:用令牌桶算法给单用户/单IP配置单接口的调用阈值,阈值按照正常业务峰值的2-3倍设置即可,超出阈值的请求直接返回
429 Too Many Requests状态码,不会透传到后端业务逻辑层。 - 处理中请求去重:对核心接口,把正在处理的请求唯一key存在Redis缓存中,过期时间设置为接口平均响应时长的2倍,相同key的新请求进来时,直接等待第一个请求的处理结果返回,不重复执行业务逻辑。
- 异常流量封禁:用滑动窗口统计调用频率,如果单用户/单IP10秒内同接口同参数请求超过20次,直接加入临时黑名单,封禁5-10分钟,同时触发运维告警,排查客户端异常。
适用算法参考
- 滑动窗口计数:实现简单、内存占用低,解决了固定窗口计数的边界突刺问题,适合做短时间的请求频率统计。
- 令牌桶算法:允许一定程度的正常突发流量,同时能把长期调用频率控制在阈值内,不会误杀正常业务的短时间并发,适合常规接口限流。
- 布隆过滤器:超大规模流量场景下,可以用布隆过滤器替代哈希表判断请求是否重复,内存开销能降低90%以上,只需要容忍极低概率的误判即可。
注意:所有拦截规则都要配置降级开关和白名单,规则上线前先做日志观察模式(只记录不拦截),确认不会误伤正常请求后再开启拦截;所有拦截触发时必须打全量日志,方便后续定位客户端bug根源。
内容的提问来源于stack exchange,提问作者maitrungdong
相关产品推荐
相关产品推荐

