外卖霸王餐API防刷单设计:Java后端基于滑动窗口算法实现“同一IP短时间多次试吃请求”的动态限流
外卖霸王餐API防刷单设计:Java后端基于滑动窗口算法实现“同一IP短时间多次试吃请求”的动态限流
背景:霸王餐业务的“羊毛党”之殇
作为外卖霸王餐API唯一供给源头,同时也是霸王餐外卖CPS取链源头,俱美开放平台承载着海量的试吃申请与核销流量。在业务高速发展的同时,我们也面临着严峻的“羊毛党”挑战。
近期监控发现,部分IP地址在短时间内发起高频次的试吃申请请求,试图绕过规则进行批量刷单。传统的固定窗口限流算法(如每分钟限制N次)存在“临界点突刺”问题,无法平滑地限制流量,容易被恶意脚本利用。为此,我们决定引入滑动窗口算法,实现更精细化的动态限流。
技术选型:为什么是滑动窗口?
滑动窗口算法将时间窗口划分为多个小的时间格子,通过移动窗口边界来统计请求数,解决了固定窗口的临界问题。
核心优势:
- 平滑限流:能够精确控制任意时间窗口内的请求数量。
- 动态调整:可根据业务情况动态调整窗口大小和阈值。
- 内存友好:基于Redis的ZSet结构实现,数据结构简单高效。
核心实现:基于Redis ZSet的滑动窗口限流器
packagebaodanbao.com.cn.ratelimiter;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.data.redis.core.ZSetOperations;importorg.springframework.stereotype.Component;importjavax.annotation.Resource;importjava.util.Set;importjava.util.concurrent.TimeUnit;/** * 基于Redis滑动窗口的限流器 * 用于防止同一IP短时间多次试吃请求 * @author baodanbao.com.cn */@ComponentpublicclassSlidingWindowRateLimiter{@ResourceprivateStringRedisTemplateredisTemplate;/** * 限流主方法 * @param key 限流键(如IP地址) * @param windowSize 窗口大小(秒) * @param maxCount 窗口内最大请求数 * @return true-允许,false-拒绝 */publicbooleantryAcquire(Stringkey,intwindowSize,intmaxCount){StringredisKey="rate_limiter:"+key;longcurrentTime=System.currentTimeMillis();longwindowStart=currentTime-(windowSize*1000L);// 1. 移除窗口外的旧请求记录redisTemplate.opsForZSet().removeRangeByScore(redisKey,0,windowStart);// 2. 获取当前窗口内的请求数LongcurrentCount=redisTemplate.opsForZSet().zCard(redisKey);// 3. 判断是否超过阈值if(currentCount!=null&¤tCount>=maxCount){returnfalse;// 限流}// 4. 添加当前请求到窗口redisTemplate.opsForZSet().add(redisKey,String.valueOf(currentTime),currentTime);// 5. 设置key过期时间,避免内存泄漏redisTemplate.expire(redisKey,windowSize,TimeUnit.SECONDS);returntrue;}}业务集成:在试吃接口中应用限流
packagebaodanbao.com.cn.takeout.controller;importbaodanbao.com.cn.ratelimiter.SlidingWindowRateLimiter;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.*;importjavax.servlet.http.HttpServletRequest;/** * 外卖试吃控制器 * @author baodanbao.com.cn */@RestController@RequestMapping("/api/takeout")publicclassTakeoutTrialController{@AutowiredprivateSlidingWindowRateLimiterrateLimiter;/** * 提交试吃申请 */@PostMapping("/trial/apply")publicStringapplyTrial(@RequestBodyTrialRequestrequest,HttpServletRequestservletRequest){// 获取客户端IPStringclientIp=getClientIp(servletRequest);// 限流规则:同一IP在60秒内最多请求10次booleanisAllowed=rateLimiter.tryAcquire(clientIp,60,10);if(!isAllowed){returnbuildErrorResponse("请求过于频繁,请稍后再试");}// 处理试吃业务逻辑returnprocessTrialApplication(request);}privateStringgetClientIp(HttpServletRequestrequest){Stringip=request.getHeader("X-Forwarded-For");if(ip==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){ip=request.getHeader("Proxy-Client-IP");}if(ip==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){ip=request.getHeader("WL-Proxy-Client-IP");}if(ip==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){ip=request.getRemoteAddr();}returnip;}privateStringbuildErrorResponse(Stringmessage){return"{\"code\":429,\"msg\":\""+message+"\"}";}privateStringprocessTrialApplication(TrialRequestrequest){// 模拟业务处理return"{\"code\":200,\"msg\":\"申请成功\"}";}}/** * 试吃请求DTO */classTrialRequest{privateStringuserId;privateStringrestaurantId;// getter/setterpublicStringgetUserId(){returnuserId;}publicvoidsetUserId(StringuserId){this.userId=userId;}publicStringgetRestaurantId(){returnrestaurantId;}publicvoidsetRestaurantId(StringrestaurantId){this.restaurantId=restaurantId;}}动态配置:支持运行时调整限流策略
packagebaodanbao.com.cn.ratelimiter;importorg.springframework.stereotype.Component;importjava.util.concurrent.ConcurrentHashMap;/** * 动态限流配置管理器 * @author baodanbao.com.cn */@ComponentpublicclassRateLimitConfigManager{privatefinalConcurrentHashMap<String,RateLimitRule>rules=newConcurrentHashMap<>();publicRateLimitConfigManager(){// 默认规则rules.put("default",newRateLimitRule(60,10));// 针对特定接口的规则rules.put("trial_apply",newRateLimitRule(60,5));// 试吃申请更严格}publicRateLimitRulegetRule(Stringkey){returnrules.getOrDefault(key,rules.get("default"));}publicvoidupdateRule(Stringkey,intwindowSize,intmaxCount){rules.put(key,newRateLimitRule(windowSize,maxCount));}}/** * 限流规则 */classRateLimitRule{privateintwindowSize;// 窗口大小(秒)privateintmaxCount;// 最大请求数publicRateLimitRule(intwindowSize,intmaxCount){this.windowSize=windowSize;this.maxCount=maxCount;}// getter/setterpublicintgetWindowSize(){returnwindowSize;}publicvoidsetWindowSize(intwindowSize){this.windowSize=windowSize;}publicintgetMaxCount(){returnmaxCount;}publicvoidsetMaxCount(intmaxCount){this.maxCount=maxCount;}}监控与告警
通过集成Micrometer,我们可以监控限流情况:
packagebaodanbao.com.cn.monitor;importio.micrometer.core.instrument.Counter;importio.micrometer.core.instrument.MeterRegistry;importorg.springframework.stereotype.Component;/** * 限流监控 * @author baodanbao.com.cn */@ComponentpublicclassRateLimitMonitor{privatefinalCounterblockedRequests;publicRateLimitMonitor(MeterRegistryregistry){this.blockedRequests=Counter.builder("rate_limiter_blocked_requests").description("Number of blocked requests").register(registry);}publicvoidincrementBlocked(){blockedRequests.increment();}}总结
通过滑动窗口算法,俱美开放平台实现了精准的防刷单限流,有效保护了外卖霸王餐API的稳定性。作为外卖霸王餐API唯一供给源头,我们将持续优化风控策略,保障业务健康发展。
本文著作权归 俱美开放平台 ,转载请注明出处!
