限流概述 系統存在服務上限,流量超過服務上限會導致系統卡死、崩潰。 限流:為了在高併發時系統穩定可用,犧牲或延遲部分請求流量以保證系統整體服務可用。 限流演算法 固定視窗計數 將時間劃分為多個視窗; 在每個視窗內每有一次請求就將計數器加一; 如果計數器超過了限制數量,則本視窗內所有的請求都被丟棄當時間 ...
限流概述
系統存在服務上限,流量超過服務上限會導致系統卡死、崩潰。
限流:為了在高併發時系統穩定可用,犧牲或延遲部分請求流量以保證系統整體服務可用。
限流演算法
- 固定視窗計數
- 將時間劃分為多個視窗;
- 在每個視窗內每有一次請求就將計數器加一;
- 如果計數器超過了限制數量,則本視窗內所有的請求都被丟棄當時間到達下一個視窗時,計數器重置。
- 滑動視窗計數
- 將時間劃分為多個區間;
- 在每個區間內每有一次請求就將計數器加一維持一個時間視窗,占據多個區間;
- 每經過一個區間的時間,則拋棄最老的一個區間,並納入最新的一個區間;
- 如果當前視窗內區間的請求計數總和超過了限制數量,則本視窗內所有的請求都被丟棄。
- 漏桶
- 將每個請求視作"水滴"放入"漏桶"進行存儲;
- "漏桶"以固定速率向外"漏"出請求來執行如果"漏桶"空了則停止"漏水";
- 如果"漏桶"滿了則多餘的"水滴"會被直接丟棄。
- 令牌桶
- 令牌以固定速率生成;
- 生成的令牌放入令牌桶中存放,如果令牌桶滿了則多餘的令牌會直接丟棄,當請求到達時,會嘗試從令牌桶中取令牌,取到了令牌的請求可以執行;
- 如果桶空了,那麼嘗試取令牌的請求會被直接丟棄。
漏桶和令牌桶對比
- 兩者實際上是相同的
- 在實現上是相同的基本演算法,描述不同。
- 給定等效參數的情況下,這兩種演算法會將完全相同的數據包視為符合或不符合。
- 兩者實現的屬性和性能差異完全是由於實現的差異造成的,即它們不是源於底層演算法的差異。
- 漏桶演算法在用作計量時,可以允許具有抖動或突發性的一致輸出數據包流,可用於流量管制和整形,並且可以用於可變長度數據包。
- 參考:
相關閱讀:
限流註解組件實現
- 利用 Spring 攔截器實現
- 使用方式:Controller 方法或類加上限流註解,請求到達攔截器時進行攔截處理
- 使用 Redis 記錄數據,Lua 保證多個命令原子性執行。
-
使用示例
@RestController @RequestMapping("/ratelimit/custom") @RateLimit(threshold = 10, rateLimiter = RateLimiterEnum.FIXED_WINDOW, time = 10, timeUnit = TimeUnit.SECONDS) public class RateLimitController { @GetMapping("/fixed/window") @RateLimit(threshold = 10, rateLimiter = RateLimiterEnum.FIXED_WINDOW, time = 10, timeUnit = TimeUnit.SECONDS) public ResponseResult<String> fixedWindow(Long id) { id += RandomUtil.randomLong(); log.info("custom:fixedWindow:{}", id); return ResponseResult.success("custom:fixedWindow:" + id); } }
-
限流註解
RateLimit.java
- 支持不同類型緩存 key:
key() + keyType()
- 支持使用不同限流演算法:
rateLimiter()
- 限流流量閾值設置:
threshold()
- 限流時長設置:
time() + timeUnit()
- 支持不同類型緩存 key:
-
限流攔截器處理 RateLimitInterceptor.java
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = ((HandlerMethod) handler); // 從方法和類上獲取註解 RateLimit annotation = AspectUtil.findMethodOrClassAnnotation(handlerMethod.getMethod(), RateLimit.class); if (annotation == null) { return true; } AspectKeyTypeEnum.KeyTypeData data = AspectKeyTypeEnum.KeyTypeData.builder() .prefix("rate:limit").key(annotation.key()).build(); String limitKey = annotation.keyType() .obtainTypeKey(handlerMethod.getMethod(), handlerMethod.getMethodParameters(), data); RateLimiterEnum limiterEnum = annotation.rateLimiter(); // 執行限流腳本 Long isLimit = redisUtil.execute(limiterEnum.obtainScript(), Lists.newArrayList(limitKey), limiterEnum.obtainArgvs(annotation).toArray()); if (isLimit != null && isLimit != 0L) { return true; } throw new ResponseException(ResponseEnum.RATE_LIMITED); }
-
限流演算法 lua 腳本
固定視窗: fixed_window_rate_limiter.lua
```lua -- 限流key ,string 保存調用限流的次數 local key = KEYS[1]; -- 最大訪問量 local capacity = tonumber(ARGV[1]); -- 限流時長(毫秒) local ttl = tonumber(ARGV[2]); local count = redis.call('INCR', key); if (count == 1) then -- 首次訪問設置過期時間 redis.call('PEXPIRE', key, ttl); end local res = 0; if (count <= capacity) then res = 1; end -- 被限流返回0,未被限流返回1 return res; ```
滑動視窗: sliding_window_rate_limiter.lua
```lua -- 限流 key , zset 保存未被限流的 id 與時間戳 local key = KEYS[1]; -- 最大訪問量 local capacity = tonumber(ARGV[1]); -- 限流時長(毫秒) local ttl = tonumber(ARGV[2]); -- 當前時間戳(毫秒) local now = tonumber(ARGV[3]); -- 唯一ID local ukid = ARGV[4]; -- 清除過期的數據 redis.call('ZREMRANGEBYSCORE', key, 0, now - ttl); local count = redis.call('ZCARD', key); local res = 0; if (count < capacity) then -- 往 zset 中添加一個值、得分均為當前時間戳的元素,[value,score] redis.call("ZADD", key, now, ukid); -- 重置 zset 的過期時間,單位毫秒 redis.call("PEXPIRE", key, ttl); res = 1; end -- 被限流返回0,未被限流返回1 return res; ```
漏桶: leaky_bucket_rate_limiter.lua
```lua -- 限流 key , hash 保存限流相關信息 local key = KEYS[1]; -- 最大訪問量 local capacity = tonumber(ARGV[1]); -- 限流時長(毫秒) local ttl = tonumber(ARGV[2]); -- 當前時間戳(毫秒) local now = tonumber(ARGV[3]); -- 水流出速率(每毫秒) local rate = tonumber(ARGV[4]); -- 限流信息 local info = redis.call("HMGET", key, "last_time", "stored_water"); -- 上次處理時間 local last_time = tonumber(info[1]); -- 當前存儲的水量,預設為0,存在保存值使用保存值 local stored_water = tonumber(info[2]); if (stored_water == nil) then stored_water = 0; end if (last_time ~= nil) then -- 根據上次處理時間和當前時間差,計算流出後的水量 local leaked_water = math.floor((now - last_time) * rate); stored_water = math.max(stored_water - leaked_water, 0); if (leaked_water > 0) then last_time = nil; end end -- 首次訪問、泄露了水 設置上次處理時間 if (last_time == nil) then redis.call("HSET", key, "last_time", now); end -- 被限流返回0,未被限流返回1 local res = 0; if (capacity > stored_water) then redis.call("HSET", key, "stored_water", stored_water + 1); res = 1; end redis.call("PEXPIRE", key, ttl); return res; ```
令牌桶: token_bucket_rate_limiter.lua
```lua -- 限流 key , hash 保存限流相關信息 local key = KEYS[1]; -- 最大訪問量 local capacity = tonumber(ARGV[1]); -- 限流時長(毫秒) local ttl = tonumber(ARGV[2]); -- 當前時間戳(毫秒) local now = tonumber(ARGV[3]); -- 生成令牌速率(每毫秒) local rate = tonumber(ARGV[4]); -- 限流信息 local info = redis.call("HMGET", key, "last_time", "stored_tokens"); -- 上次處理時間 local last_time = tonumber(info[1]); -- 令牌數量,預設為最大訪問量,存在保存值使用保存值 local stored_tokens = tonumber(info[2]); if (stored_tokens == nil) then stored_tokens = capacity; end if (last_time ~= nil) then -- 根據上次處理時間和當前時間差,觸髮式往桶里添加令牌 local add_tokens = math.floor((now - last_time) * rate); stored_tokens = math.min(add_tokens + stored_tokens, capacity); if (add_tokens > 0) then last_time = nil; end end -- 首次訪問、添加了令牌 設置上次處理時間 if (last_time == nil) then redis.call("HSET", key, "last_time", now); end -- 被限流返回0,未被限流返回1 local res = 0; if (stored_tokens > 0) then redis.call("HSET", key, "stored_tokens", stored_tokens - 1); res = 1; end redis.call("PEXPIRE", key, ttl); return res; ```
其他
demo 地址:https://github.com/EastX/java-practice-demos/tree/main/demo-ratelimit
推薦閱讀:
作者:EastX本文來自博客園,轉載請註明原文鏈接:https://www.cnblogs.com/cnx01/p/16861415.html