高并发场景下,接口防刷是系统稳定性的重要保障。上一篇我们已经介绍了固定计数器限流和令牌桶限流,今天继续分享漏桶限流和滑动时间窗口限流这两种经典算法。
一、漏桶限流 1、什么是漏桶限流 漏桶算法(Leaky Bucket)是一种经典的流量整形算法,它模拟一个底部有固定大小出水口的桶。无论流入速度多快,流出速率都保持恒定,这确保了流量的平滑输出。
核心特点:
固定流出速率 :无论输入多么不稳定,输出始终保持恒定速率
桶容量限制 :当桶满时,新请求会被拒绝
流量整形 :能够将突发流量转换为平稳流量
适用场景:
需要稳定输出速率的场景,如消息队列消费
保护下游系统不被突发流量冲垮
需要严格控制流出速率的API接口
2、代码实现 ①、自定义防刷注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface LeakyBucketLimit { String key () default "" ; int capacity () default 100 ; int rate () default 10 ; }
②、Lua脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 local key = KEYS[1 ]local capacity = tonumber (ARGV[1 ])local rate = tonumber (ARGV[2 ])local now = tonumber (ARGV[3 ])local requestCount = tonumber (ARGV[4 ])local bucketInfo = redis.call('hmget' , key, 'water' , 'lastLeakTime' )local currentWater = 0 local lastLeakTime = nowif bucketInfo[1 ] then currentWater = tonumber (bucketInfo[1 ]) end if bucketInfo[2 ] then lastLeakTime = tonumber (bucketInfo[2 ]) end local leakAmount = (now - lastLeakTime) * rateif leakAmount > 0 then currentWater = math .max (0 , currentWater - leakAmount) lastLeakTime = now end if currentWater + requestCount <= capacity then currentWater = currentWater + requestCount redis.call('hmset' , key, 'water' , currentWater, 'lastLeakTime' , lastLeakTime) redis.call('expire' , key, 3600 ) return 1 else return 0 end
③、Lua脚本配置 1 2 3 4 5 6 7 8 9 10 @Bean public RedisScript<Long> leakyBucketScript () { DefaultRedisScript<Long> redisScript = new DefaultRedisScript <>(); redisScript.setLocation(leakyLuaFile); redisScript.setResultType(Long.class); return redisScript; }
④、漏桶限流服务类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Slf4j @Service public class LeakyBucketRateLimiter { @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private RedisScript<Long> leakyBucketScript; public boolean tryAcquire (String key, int capacity, int rate, int requestCount) { long now = System.currentTimeMillis() / 1000 ; Long result = redisTemplate.execute(leakyBucketScript, Collections.singletonList(key), capacity,rate,now,requestCount); return result != null && result == 1 ; } }
⑤、漏桶限流切片核心逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 @Slf4j @Aspect @Component public class LeakyBucketLimitAspect { @Autowired private LeakyBucketRateLimiter rateLimiter; @Around("@annotation(leakyBucketLimit)") public Object around (ProceedingJoinPoint joinPoint, LeakyBucketLimit leakyBucketLimit) throws Throwable { String key = buildRateLimitKey(joinPoint, leakyBucketLimit); int capacity = leakyBucketLimit.capacity(); int rate = leakyBucketLimit.rate(); LeakyBucketRateLimiter.BucketStatus bucketStatus = rateLimiter.getBucketStatus(key); log.debug("bucket status: key={}, water={},lastLeakTime={},ttl={}" ,key, bucketStatus.getCurrentWater(), bucketStatus.getLastLeakTime(), bucketStatus.getTtl()); if (!rateLimiter.tryAcquire(key, capacity, rate, 1 )) { throw new ApiException (ApiResultEnum.REQUEST_LIMIT); } return joinPoint.proceed(); } private String buildRateLimitKey (ProceedingJoinPoint joinPoint, LeakyBucketLimit rateLimit) { String key = rateLimit.key(); if (key.isEmpty()) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); String className = method.getDeclaringClass().getSimpleName(); String methodName = method.getName(); String userKey = getCurrentUserId(); return String.format("leaky_bucket:%s:%s:%s" , className, methodName, userKey); } if (key.contains("#" )) { return AopUtil.parseSpel(key, joinPoint); } return key; } private String getCurrentUserId () { return "user123" ; } }
⑥、controller示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @RestController @RequestMapping("/leakyRate") public class LeakyRateController { @Autowired private LeakyBucketRateLimiter leakyBucketRateLimiter; @GetMapping("/test1") @LeakyBucketLimit(rate = 1, capacity = 3) public R test1 () { return R.ok(); } @GetMapping("/test2") @LeakyBucketLimit(key = "leaky_rate:test2",rate = 2, capacity = 1) public R test2 () { return R.ok(); } @LeakyBucketLimit(key = "'user :' + #username", rate = 2, capacity = 10) @GetMapping("/search") public R search (@RequestParam String username) { return R.ok("username:" + username); } }
二、滑动时间窗口限流 1、什么是滑动时间窗口限流 滑动时间窗口算法将时间线划分为多个小的时间片段,通过滑动窗口的方式统计最近一段时间内的请求次数。相比固定窗口,它能更好地处理时间边界问题。
算法优势:
平滑过渡 :解决了固定窗口在边界时间的突发问题
精准控制 :精确统计每个时间窗口内的请求数
灵活配置 :支持任意时间窗口大小
适用场景:
API调用频率限制
用户行为频率控制
需要精确时间控制的限流场景
2、代码实现 ①、自定义防刷注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SlidingWindowLimit { String key () default "" ; int window () default 60 ; int maxCount () default 100 ; }
②、Lua脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 local key = KEYS[1 ]local window = tonumber (ARGV[1 ])local maxCount = tonumber (ARGV[2 ])local now = tonumber (ARGV[3 ])local requestCount = tonumber (ARGV[4 ]) or 1 local windowStart = now - windowredis.call('zremrangebyscore' , key, 0 , windowStart) local currentCount = redis.call('zcard' , key)if currentCount + requestCount <= maxCount then for i = 1 , requestCount do local member = now * 1000 + math .random (0 , 999 ) redis.call('zadd' , key, member, member) end redis.call('expire' , key, window + 1 ) return 1 else return 0 end
③、Lua脚本配置 1 2 3 4 5 6 7 8 9 10 @Bean public RedisScript<Long> slidingWindowScript () { DefaultRedisScript<Long> redisScript = new DefaultRedisScript <>(); redisScript.setLocation(slidingWindowLuaFile); redisScript.setResultType(Long.class); return redisScript; }
④、滑动时间窗口限流服务类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Slf4j @Service public class SlidingWindowRateLimiter { @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private RedisScript<Long> slidingWindowScript; public boolean tryAcquire (String key, int window, int maxCount, int requestCount) { long now = System.currentTimeMillis() / 1000 ; Long result = redisTemplate.execute(slidingWindowScript, Collections.singletonList(key), window, maxCount, now, requestCount); return result != null && result == 1 ; } }
⑤、滑动时间窗口限流切片核心逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 @Slf4j @Aspect @Component public class SlidingWindowLimitAspect { private final SlidingWindowRateLimiter rateLimiter; public SlidingWindowLimitAspect (SlidingWindowRateLimiter rateLimiter) { this .rateLimiter = rateLimiter; } @Around("@annotation(slidingWindowLimit)") public Object around (ProceedingJoinPoint joinPoint, SlidingWindowLimit slidingWindowLimit) throws Throwable { String key = buildRateLimitKey(joinPoint, slidingWindowLimit); int window = slidingWindowLimit.window(); int maxCount = slidingWindowLimit.maxCount(); if (!rateLimiter.tryAcquire(key, window, maxCount, 1 )) { throw new ApiException (ApiResultEnum.REQUEST_LIMIT); } return joinPoint.proceed(); } private String buildRateLimitKey (ProceedingJoinPoint joinPoint, SlidingWindowLimit rateLimit) { String key = rateLimit.key(); if (key.isEmpty()) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); String className = method.getDeclaringClass().getSimpleName(); String methodName = method.getName(); String userKey = getCurrentUserId(); return String.format("sliding_window:%s:%s:%s" , className, methodName, userKey); } if (key.contains("#" )) { return AopUtil.parseSpel(key, joinPoint); } return key; } private String getCurrentUserId () { return "user123" ; } }
⑥、controller示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @RestController @RequestMapping("/SlidingWindowRate") public class SlidingWindowRateController { @Autowired private SlidingWindowRateLimiter slidingWindowRateLimiter; @GetMapping("/test1") @SlidingWindowLimit(window = 3, maxCount = 1) public R test1 () { return R.ok(); } @GetMapping("/test2") @SlidingWindowLimit(key = "sliding_window:test2",window = 60, maxCount = 5) public R test2 () { return R.ok(); } @SlidingWindowLimit(key = "'user :' + #username") @GetMapping("/search") public R search (@RequestParam String username) { return R.ok("username:" + username); } }
三、总结 限流算法选择指南
算法类型
适用场景
优点
缺点
漏桶限流
需要稳定输出速率、保护下游系统
输出稳定、平滑流量
无法应对突发流量、响应延迟
滑动窗口
精确控制时间段内的请求次数
边界处理优秀、精确控制
实现相对复杂、内存占用较多
令牌桶
允许一定程度的突发流量
支持突发流量、灵活性高
可能瞬间打满系统
固定窗口
简单计数场景、对精度要求不高
实现简单、性能高
边界时间可能超限
资源获取: 本文完整代码已上传至 GitHub,欢迎 Star ⭐ 和 Fork
原创不易,如有收获,请点赞收藏支持! 🔥