NestJS+Redis實現(xiàn)手寫一個限流器
前言
限流是大型系統(tǒng)必備的保護措施,常用的限流算法主要有固定時間窗口,滑動時間窗口,漏桶,令牌桶等。本文將會寫道的方案是使用 滑動時間窗口 算法,通過拒絕請求的方式來達到限流的目的。 本文的實現(xiàn)方式是 redis
, lua 腳本
以及 Nestjs Guard
來實現(xiàn) 限流的效果。
概念淺析
這里簡單說一下 固定時間窗口 和滑動時間窗口的概念
固定時間窗口
它可以解決 每 時間單位(可以是秒或者分鐘等等),允許訪問的次數(shù)。但是無法控制頻率。舉例1分鐘允許訪問100 次,可能前10 秒訪問了90次,后面只有10次機會了。 還有一個問題就是在兩個時間單位的臨界值上可能會超出閾值,繼續(xù)用前面的例子,第59秒訪問了60次,第二個時間單位前10秒訪問了50 次,在橫跨兩個時間單位的20秒中,超出了閾值 (110>100)
滑動時間窗口
可以改善固定窗口的所帶來超出閾值的問題。它將每個單位之間分割成若干小周期,當前時間單位不再是固定的,而是根據(jù)當前請求時間往后移動,即所謂滑動窗口。每個周期分的越小,限流控制的越精細。
具體實現(xiàn)
使用的主要包的版本 nestjs 8.0.0
ioredis 5.3.2
我們主要實現(xiàn)以下幾個東西
- 一個 guard 文件 用于實現(xiàn)限流的業(yè)務邏輯
- 一個 decorator文件 , 裝飾器,用于設置當前接口限流的頻率,允許訪問次數(shù)等字段
- 一個 redis 類 和一個lua 腳本
redis 相關
主要就是通過lua 腳本進行計數(shù),達到限流的目的。這里做了一個優(yōu)化,對執(zhí)行l(wèi)ua 取了hash 值,在redis 運行一次后 ,可以使用evalsha 直接運行腳本,避免二次載入腳本。
import { Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { ConfigService } from '@app/common'; import { createHash } from 'crypto' import { v4 as uuidv4 } from 'uuid'; const rateLimitScript = ""http:// 后面單獨列出 @Injectable() export class RedisService { private readonly redisClient: Redis.Redis; private luaScript: any; constructor( private readonly configService: ConfigService, ) { const self = this; const connConfig = this.configService.get("redisService") this.redisClient = new Redis.Redis(connConfig) this.luaScript = { rateLimit: { script: rateLimitScript, hash: self.hashStr(rateLimitScript) }, } } private hashStr(value: string) { return createHash("sha1").update(value).digest('hex') } async rateLimit(opts: any): Promise<boolean> { const { key, limit, windowSize } = opts; const uuid = uuidv4() let result; const { script, hash } = this.luaScript.rateLimit try { const shaResult = await this.redisClient.evalsha(hash, 1, key, limit, windowSize, uuid) result = shaResult } catch (error) { const shaResult = await this.redisClient.eval(script, 1, key, limit, windowSize, uuid) result = shaResult } return result == 1 } }
接下來展示lua 腳本
--傳入四個參數(shù) 分別是key,限制次數(shù),時間范圍,唯一值 local key = KEYS[1] local limit = tonumber(ARGV[1]) local windowSize = tonumber(ARGV[2]) --單位毫秒 local uuid = ARGV[3] -- 唯一值是為了防止zset 重復 -- 使用redis 來獲取時間,防止多進程生成相似的邊界導致超頻。時間單位是微秒 local date = redis.call("time") local now = tonumber(date[1]) * 1000000 + tonumber(date[2]) local startTime = now - windowSize * 1000 local endTime = now + 1000000 -- 計算過期時間 時間單位是秒 local expireSec = tonumber(math.ceil(windowSize / 1000)) + 1 -- 統(tǒng)計當前zset數(shù)組里的數(shù)據(jù),超出范圍則返回0, -- 否則做3件事,然后返回1 -- 1、向數(shù)組里增加新值 -- 2、刪除數(shù)組中開始時間之前的數(shù)據(jù),防止數(shù)組過大 -- 3、給數(shù)組續(xù)過期時間 local count = tonumber(redis.call('zcount', key, startTime, endTime)) if count + 1 > limit then return 0 else redis.call('zadd', key, now, uuid) redis.call('zremrangebyscore', key, 0, startTime - 100000) redis.call('expire', key, expireSec) return 1 end
裝飾器相關
這個很簡單就是,設置一下redis 鍵值的前綴,允許訪問的次數(shù)和 單位之間的長度。在這里設置了之后可以在 guard 里通過反射拿到這些值
import { SetMetadata } from '@nestjs/common'; export interface rateLimitOptions { keyPrefix: string, limit: number, windowSize: number } export const RateLimit = (options: rateLimitOptions): MethodDecorator => SetMetadata('rateLimit', options)
guard 相關
guard 就是把之前的部分整合了一下,如果當前接口沒有設置限流參數(shù)則啟用默認參數(shù),keyprefix 取當前接口的路徑。
import { Injectable, ExecutionContext, CanActivate } from "@nestjs/common"; import { Reflector } from '@nestjs/core'; import { RedisService, rateLimitOptions } from "@app/common"; import { BusinessException } from "@app/common"; @Injectable() export class RateLimitGuard implements CanActivate { constructor( private reflector: Reflector, private redisService: RedisService ) { } private getIpFromRequest(request: { ip: string }): string { return request.ip?.match(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/)?.[0] } async canActivate(context: ExecutionContext) { // 通過反射拿到前面設置的值 const rateLimitConfig = this.reflector.get<rateLimitOptions>("rateLimit", context.getHandler()); if (!rateLimitConfig) { // 當前接口如果沒設置參數(shù)則定義默認參數(shù) const cMethod = this.reflector.get("method", context.getHandler());// 是GET,POST 等http method const cPath = this.reflector.get("path", context.getHandler());// 接口的具體路徑 rateLimitConfig = { keyPrefix: cMethod + ":" + cPath, limit: 1, windowSize: 5000 } } const { keyPrefix, limit, windowSize } = rateLimitConfig const request = context.switchToHttp().getRequest(); const ip = this.getIpFromRequest(request) const key = keyPrefix + ":" + ip const isPass = await this.redisService.rateLimit({ key, limit, windowSize }) if (!isPass) { // 返回自定義的錯誤 throw new BusinessException("RATE_LIMIT_EXCEEDED_LIMIT") } return true } }
使用方法
引入guand 和RateLimit 裝飾器,可以給特定路由增加限流保護
@Controller('user') export class UserController { constructor(private readonly userService: UserService) { } @Public() @RateLimit({ keyPrefix: "login", limit: 3, windowSize: 1000 }) @UseGuards(RateLimitGuard) @Post('register') register(@Body() createUserDto: CreateUserDto) { return this.userService.register(createUserDto); } }
或者基于模塊的也可以,這樣路由里的就可以省略了,如果某些接口沒設置RateLimit 參數(shù),guard 內(nèi)部就會使用默認統(tǒng)一參數(shù)。
@Module({ providers: [ { provide: APP_GUARD, useClass: RateLimitGuard } ], })
使用ab 測試一下結(jié)果,為了便于測試設置為每5秒可以請求3次。用 ab
進行兩次測試,結(jié)果如下
2023-11-25 17:50:39 - error - HttpExceptionFilter - d1385d48-183c-4fbf-b751-4d0b6786f5ba : {"validatorCode":10005,"validatorMessage":"用戶已存在"} - {}
2023-11-25 17:50:39 - error - HttpExceptionFilter - 65f0e427-92e4-4854-a6cc-116c70daac61 : {"validatorCode":10005,"validatorMessage":"用戶已存在"} - {}
2023-11-25 17:50:39 - error - HttpExceptionFilter - b24b8f7e-f961-45d1-a909-36219fc5d112 : {"validatorCode":10005,"validatorMessage":"用戶已存在"} - {}
2023-11-25 17:50:39 - error - HttpExceptionFilter - 9c65d452-8eeb-4c40-a76e-b5bf01524ebb : {"validatorCode":30000,"validatorMessage":"請求頻率過快"} - {}
2023-11-25 17:50:39 - error - HttpExceptionFilter - 1065a900-bb55-4514-9b55-08cc57509e37 : {"validatorCode":30000,"validatorMessage":"請求頻率過快"} - {}
2023-11-25 17:50:45 - error - HttpExceptionFilter - a8176f1c-2788-4e2a-8267-4e2ffadb6238 : {"validatorCode":10005,"validatorMessage":"用戶已存在"} - {}
2023-11-25 17:50:45 - error - HttpExceptionFilter - db911d44-ee8b-4da2-a87b-fa0bcb433c45 : {"validatorCode":10005,"validatorMessage":"用戶已存在"} - {}
2023-11-25 17:50:45 - error - HttpExceptionFilter - 3c59e335-441b-4907-80e4-0d807e5bfb01 : {"validatorCode":10005,"validatorMessage":"用戶已存在"} - {}
2023-11-25 17:50:45 - error - HttpExceptionFilter - 6f48108b-bc8e-4fb6-8231-fa5a9cd22b5f : {"validatorCode":30000,"validatorMessage":"請求頻率過快"} - {}
2023-11-25 17:50:45 - error - HttpExceptionFilter - 7c77c1df-bfe6-4f72-b75a-0d9a1211fe64 : {"validatorCode":30000,"validatorMessage":"請求頻率過快"} - {}
達到要求,收工。
以上就是NestJS+Redis實現(xiàn)手寫一個限流器的詳細內(nèi)容,更多關于NestJS Redis限流器的資料請關注腳本之家其它相關文章!
相關文章
在Ubuntu?14.04系統(tǒng)上備份和恢復Redis數(shù)據(jù)詳細步驟
這篇文章主要給大家介紹了關于在Ubuntu?14.04系統(tǒng)上備份和恢復Redis數(shù)據(jù)的詳細步驟,文中通過代碼介紹的非常詳細,對大家學習或者使用Redis具有一定的參考借鑒價值,需要的朋友可以參考下2024-04-04Redis連接池監(jiān)控(連接池是否已滿)與優(yōu)化方法
本文詳細講解了如何在Linux系統(tǒng)中監(jiān)控Redis連接池的使用情況,以及如何通過連接池參數(shù)配置、系統(tǒng)資源使用情況、Redis命令監(jiān)控、外部監(jiān)控工具等多種方法進行檢測和優(yōu)化,以確保系統(tǒng)在高并發(fā)場景下的性能和穩(wěn)定性,討論了連接池的概念、工作原理、參數(shù)配置,以及優(yōu)化策略等內(nèi)容2024-09-09Redis中l(wèi)ua腳本實現(xiàn)及其應用場景
本文主要介紹了Redis中l(wèi)ua腳本實現(xiàn)及其應用場景,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-04-04RedisTemplate 實現(xiàn)基于Value 操作的簡易鎖機制(示例代碼)
本文將介紹如何使用 RedisTemplate 的 opsForValue().setIfAbsent() 方法來實現(xiàn)一種簡單的鎖機制,并提供一個示例代碼,展示如何在 Java 應用中利用這一機制來保護共享資源的訪問,感興趣的朋友跟隨小編一起看看吧2024-05-05詳解Redis中的BigKey如何發(fā)現(xiàn)和處理
這篇文章主要為大家詳細介紹了Redis中的BigKey如何發(fā)現(xiàn)和處理,文中給大家詳細講解了BigKey危害和如何解決這些問題,文章通過代碼示例和圖文介紹的非常詳細,需要的朋友可以參考下2023-10-10