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 相關(guān)
主要就是通過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
裝飾器相關(guān)
這個很簡單就是,設置一下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 相關(guān)
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)容,更多關(guān)于NestJS Redis限流器的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
在Ubuntu?14.04系統(tǒng)上備份和恢復Redis數(shù)據(jù)詳細步驟
這篇文章主要給大家介紹了關(guān)于在Ubuntu?14.04系統(tǒng)上備份和恢復Redis數(shù)據(jù)的詳細步驟,文中通過代碼介紹的非常詳細,對大家學習或者使用Redis具有一定的參考借鑒價值,需要的朋友可以參考下2024-04-04
Redis連接池監(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-09
Redis中l(wèi)ua腳本實現(xiàn)及其應用場景
本文主要介紹了Redis中l(wèi)ua腳本實現(xiàn)及其應用場景,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-04-04
RedisTemplate 實現(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

