自FireflySoft.RateLimit發(fā)布以來,幫助了不少需要在.net中進(jìn)行限流處理的用戶。前段時(shí)間有個(gè)開發(fā)者發(fā)了一個(gè)pull request,大意是Redis重啟的時(shí)候Lua script會(huì)丟失,但是程序中還認(rèn)為它存在,所以就會(huì)一直拋出異常,那位同學(xué)通過捕捉一個(gè)特定異常再reload Lua script的方式解決了這個(gè)問題。經(jīng)過一段時(shí)間的測(cè),試運(yùn)行良好,因?yàn)檫@個(gè)問題還是相對(duì)常見的,所以就發(fā)布了一個(gè)版本 2.0.2,建議通過nuget盡快升級(jí)。
之前還有用戶問怎么在程序執(zhí)行過程中動(dòng)態(tài)更改限流的閾值,比如原來限流100/s,現(xiàn)在服務(wù)性能更好了,要改成限流300/s。FireflySoft.RateLimit底層是支持的,通過IAlgorithm.UpdateRules或者UpdateRulesAsync即可實(shí)現(xiàn)。不過這只是開放了一個(gè)基礎(chǔ)能力,實(shí)際還需要開發(fā)者自己去做更多的工作,比如定義限流閾值的數(shù)據(jù)格式、從其它配置系統(tǒng)中定時(shí)獲取最新的限流閾值等。為了更方便開發(fā)者使用這個(gè)類庫(kù),同時(shí)恰逢.NET 6正式發(fā)布,所以這里用.NET6編寫一個(gè)Demo程序,可以實(shí)現(xiàn)程序運(yùn)行時(shí)動(dòng)態(tài)更新限流閾值。
限流需求
這里假設(shè)需求是這樣的:
- 有一個(gè)天氣服務(wù),包含兩個(gè)接口:GetToday(獲取今天的天氣)、GetTomorrow(獲取明天的天氣)。
- 對(duì)每個(gè)訪問者分別單獨(dú)限流,具體限流閾值:GetToday 20次/秒、GetTomorrow 10次/秒,所有接口總計(jì) 25次/秒。
- 每秒的訪問次數(shù)并不均勻,有一定的突發(fā)請(qǐng)求。大部分情況下低于限流閾值,極少數(shù)時(shí)可能會(huì)超出限流閾值30%。
限流配置
FireflySoft.RateLimit中不同的限流算法有不同的限流規(guī)則定義,因?yàn)橛型话l(fā)情況,所以這里采用令牌桶算法。根據(jù)限流需求,這里定義了一個(gè)限流配置,它是應(yīng)用到每一個(gè)用戶的。
public class RateLimitConfiguration
{
public string? Path { get; set; }
public LimitPathType PathType { get; set; }
public int TokenCapacity { get; set; }
public int TokenSpeed { get; set; }
}
其中:
- Path 用來定義接口路徑,形如:/WeatherForecast/GetToday
- PathType 指定應(yīng)用到的接口類型:?jiǎn)蝹€(gè)接口還是所有接口
- TokenCapacity 是令牌桶容量
- TokenSpeed 是令牌放入速度,這里固定單位是:個(gè)/秒,F(xiàn)ireflySoft.RateLimit支持更小的時(shí)間單位。
同時(shí)為了方便限流規(guī)則的更新,它可以用來傳輸或者持久化到各種存儲(chǔ)中。我把配置保存在MySQL中,更改限流閾值時(shí)更新數(shù)據(jù)庫(kù)內(nèi)容,應(yīng)用限流閾值時(shí)從數(shù)據(jù)庫(kù)中查詢限流閾值。你也可以把這個(gè)配置放到任何其它地方,比如Consul、Redis,甚至配置文件中。
處理架構(gòu)
為了描述的更清晰,我這里提供一張圖:

如上圖所示,業(yè)務(wù)服務(wù)集成了限流功能,核心模塊有兩個(gè):
- 限流處理:這個(gè)直接集成FireflySoft.RateLimit.AspNetCore即可實(shí)現(xiàn)。
- 監(jiān)控配置變更:這是單獨(dú)擴(kuò)展的部分,主要邏輯是:讀取數(shù)據(jù)庫(kù)中的限流規(guī)則配置,如果有變化,則調(diào)用FireflySoft.RateLimit的限流規(guī)則更新接口。
其它模塊:
- 構(gòu)造錯(cuò)誤:這個(gè)也是FireflySoft.RateLimit.AspNetCore自帶的功能,可以自定義錯(cuò)誤碼和錯(cuò)誤消息內(nèi)容。
- 限流配置更改程序:這里沒有實(shí)現(xiàn)。功能就是更改數(shù)據(jù)庫(kù)中的限流規(guī)則配置,我們測(cè)試直接改數(shù)據(jù)庫(kù)就行了。
編寫代碼
這里寫了一個(gè)基于.Net6 的 WebAPI demo,項(xiàng)目結(jié)構(gòu)如下圖,你也可以直接點(diǎn)開查看:samples/aspnetcore6 (github.com)

為了方便集成到自己的項(xiàng)目中,這里也寫一下具體的使用步驟:
1、創(chuàng)建或打開你的項(xiàng)目
打開項(xiàng)目,你可以用Visual Studio,也可以用Visual Studio Code。
如果項(xiàng)目是.NET Framework,必須是4.6.1及以上。如果是.NET Core,必須是2.0及以上。這里是.NET6。
2、安裝Nuget包
你可以使用Package Manager:
Install-Package FireflySoft.RateLimit.AspNetCore -Version 2.0.2-rc1
也可使用.NET CLI:
dotnet add package FireflySoft.RateLimit.AspNetCore --version 2.0.2-rc1
3、配置數(shù)據(jù)庫(kù)表
你需要有一個(gè)MySQL,我建議是5.7及以上,下邊是創(chuàng)建表和測(cè)試配置的SQL腳本:
CREATE TABLE `rate_limit_rule` (
`Id` varchar(40) NOT NULL,
`Path` varchar(100) NOT NULL,
`PathType` int(11) NOT NULL,
`TokenCapacity` int(11) NOT NULL,
`TokenSpeed` int(11) NOT NULL,
`AddTime` datetime NOT NULL,
`UpdateTime` datetime NOT NULL,
PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
INSERT INTO rate_limit_rule (Id,`Path`,PathType,TokenCapacity,TokenSpeed,AddTime,UpdateTime) VALUES
('1','/WeatherForecast/GetToday',1,26,20,'2021-11-16 00:00:00.0','2021-11-16 00:00:00.0'),
('2','/WeatherForecast/GetTomorrow',1,13,10,'2021-11-16 00:00:00.0','2021-11-16 00:00:00.0'),
('3','All',2,29,25,'2021-11-16 00:00:00.0','2021-11-16 00:00:00.0');
打開項(xiàng)目中的appsettings.json,添加一個(gè)DbConn的配置項(xiàng):
{
"DbConn":"Server=127.0.0.1;User ID=root;Password=l123456;port=3306;Database=ratelimit;CharSet=utf8mb4;",
...
}
這里邊的數(shù)據(jù)庫(kù)地址、數(shù)據(jù)庫(kù)名稱、帳號(hào)密碼、字符集都需要改成自己的。
你也可以使用其它的數(shù)據(jù)庫(kù)連接配置方式,比如放到Consul中,或者寫到自己的配置中心,甚至寫死在代碼中。
4、編寫”監(jiān)控配置變更“
在上邊的架構(gòu)圖中,提到一個(gè)”監(jiān)控配置變更“的部分,這個(gè)是這篇文章的重頭戲。FireflySoft.RateLimit自身沒有提供這部分,需要根據(jù)需求自己實(shí)現(xiàn)。我這里提供一個(gè)實(shí)現(xiàn)方案,僅供參考。
這個(gè)部分我寫了5個(gè)文件:
- RateLimitRuleDAO.cs:實(shí)現(xiàn)從數(shù)據(jù)庫(kù)查詢出限流規(guī)則配置。
- RateLimitConfigurationManager.cs:實(shí)現(xiàn)跟蹤數(shù)據(jù)庫(kù)中的限流配置變更,如果有變更則觸發(fā)一個(gè)事件。
- NonCapturingTimer.cs:用于定時(shí)查詢數(shù)據(jù)庫(kù)中的限流配置。不捕捉上下文的Timer,用習(xí)慣了而已。
- AutoUpdateAlgorithmManager.cs:注冊(cè)事件到RateLimitConfigurationManager中,事件發(fā)生時(shí)更新到限流算法中。
- AutoUpdateAlgorithmService.cs:方便注冊(cè)服務(wù):向ASP.NET Core中注冊(cè)上邊這幾個(gè)服務(wù)。
代碼量比較大,這里就不貼了,可以到Github上查看詳細(xì)。
5、注冊(cè)服務(wù)和使用中間件
.NET6中這部分要寫到Program.cs中,限于篇幅,這里省略了很多代碼,只需要關(guān)注如下幾行:
- builder.Services.AddAutoUpdateRateLimitAlgorithm 這個(gè)在AutoUpdateAlgorithmService.cs中定義的。
- builder.Services.AddRateLimit 這個(gè)是FireflySoft.RateLimit.AspNetCore定義的。
- app.UseRateLimit() 這個(gè)是FireflySoft.RateLimit.AspNetCore定義的。
using aspnetcore6.RateLimit;
using FireflySoft.RateLimit.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
...
// Add firefly soft rate limit service
builder.Services.AddAutoUpdateRateLimitAlgorithm();
builder.Services.AddRateLimit(serviceProvider =>
{
var algorithmManager = serviceProvider.GetService<AutoUpdateAlgorithmManager>();
if (algorithmManager != null)
{
return algorithmManager.GetAlgorithmInstance();
}
return null;
});
var app = builder.Build();
...
// Use firefly soft rate limit middleware
app.UseRateLimit();
app.MapControllers();
app.Run();
6、啟動(dòng)服務(wù)并測(cè)試
可以使用Postman來運(yùn)行一個(gè)Runner,執(zhí)行100次,看看實(shí)際效果。

關(guān)于.NET6
雖然標(biāo)題中提到了.NET6,不過到目前為止還沒看到什么關(guān)于.NET6的特別內(nèi)容,所以這里特別準(zhǔn)備了一點(diǎn)關(guān)于.NET6的內(nèi)容,否則就太標(biāo)題黨了。
如果你使用過.NET Core,其實(shí).NET6用起來也沒有太多變化,很多.net core、.net standard的庫(kù)也都兼容,這里列舉兩點(diǎn)我感覺變化比較大的地方:
Namespace
現(xiàn)在namespace可以直接聲明應(yīng)用到整個(gè)文件,不需要再加大括號(hào),被括號(hào)層級(jí)折磨的人輕松了。
using System.Collections.ObjectModel;
namespace aspnetcore6.RateLimit;
public class RateLimitConfiguration
{
public string? Path { get; set; }
public LimitPathType PathType { get; set; }
public int TokenCapacity { get; set; }
public int TokenSpeed { get; set; }
}
Top-level statements
Program.cs和Startup.cs的內(nèi)容合并到Program.cs中了,并且不需要顯式編寫main方法,直接一行行的寫就行了。這樣確實(shí)又簡(jiǎn)便了一些。主要內(nèi)容還是那兩部分:構(gòu)建Web應(yīng)用(注冊(cè)服務(wù)、使用中間件)、運(yùn)行Web應(yīng)用。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
...
var app = builder.Build();
...
app.MapControllers();
app.Run();
不過一個(gè)應(yīng)用中只能有一個(gè)這樣的文件,你也不能再寫其它main方法作為程序的入口點(diǎn)。