用更简单的话来说,速率限制 是一种技术,限制用户或客户端在一定时间内对API的请求数量。你可能以前访问过天气预报或笑话API时遇到过“速率限制已超出”的消息。关于为什么要对API进行速率限制,有很多理由,其中一些重要的是确保安全、防止资源超载、公平使用等。
在这篇博客中,我们将创建一个使用Golang和Gin框架的HTTP服务器,并使用Redis为某个端点添加请求速率限制的功能。我们还将记录某个IP地址在一段时间内对服务器的请求总次数,并在请求次数超过我们设定的限制时返回一个错误消息。
如果你还不了解Gin和Redis的话。Gin 是一个用Golang编写的web框架。它可以帮助你快速构建一个简单的服务器,无需写出大量代码。Redis 是一个内存中的键值数据存储系统,可以作为数据库或缓存层使用。
前提条件
- 熟悉 Golang、Gin 和 Redis
- 一个 Redis 实例(可以使用 Docker 或远程机器来运行)
开始入门:让我们开始吧
要初始化项目,运行 go mod init <GitHub 路径>
,例如 go mod init github.com/Pradumnasaraf/go-redis
。
那么让我们来创建一个简单的HTTP服务器,用Gin框架,然后为其应用速率限制逻辑。下面的代码你可以复制。这非常基础。当请求/message
端点时,服务器会回复消息。
复制下方代码后,,然后就可以执行 go mod tidy
命令(这个命令会整理并安装我们用到的那些包)。
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/message", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "你可以继续发起更多请求",
})
})
r.Run(":8081") // Run on localhost:8081
}
切换到全屏退出全屏
我们可以在终端中输入 go run main.go
并运行,然后在终端中看到消息。
点击这里查看图片!
要测试它,只需在浏览器中访问 localhost:8081/message
,就能看到消息。
浏览器截图如下:
现在我们的服务器已经启动并运行了,让我们为 /message
路由设置一个限流功能。我们将使用 go-redis/redis_rate
包。感谢这个包的创建者,我们不需要自己动手写处理和检查限制的逻辑。它会为我们搞定这一切。
以下实现了限流功能后的完整代码。我们将逐部分理解代码。一开始就提供完整代码是为了避免混淆,并理解不同部分是如何协同作用的。
一旦你复制代码后,请运行 go mod tidy
来安装所有导入的包。现在让我们来理解代码,下面我们将一起看这段代码。
package main
import (
"context"
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis_rate/v10"
"github.com/redis/go-redis/v9"
)
func main() {
r := gin.Default()
r.GET("/message", func(c *gin.Context) {
err := rateLimiter(c.ClientIP())
if err != nil {
c.JSON(http.StatusTooManyRequests, gin.H{
"message": "您已达速率限制",
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "您可以继续发送更多请求",
})
})
r.Run(":8081")
}
func rateLimiter(clientIP string) error {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
limiter := redis_rate.NewLimiter(rdb)
res, err := limiter.Allow(ctx, clientIP, redis_rate.PerMinute(10))
if err != nil {
panic(err)
}
if res.Remaining == 0 {
return errors.New("您已达速率限制")
}
return nil
}
进入全屏,退出全屏
让我们直接跳到 rateLimiter()
函数并理解它。这个函数需要一个参数,即请求的 IP 地址,我们可以通过 c.ClientIP()
在 main
函数中获取。如果达到限制,则返回一个错误,否则返回 nil
。大部分代码是从官方 GitHub repo 中复制来的样板代码。这里的关键功能是 limiter.Allow()
函数。Addr:
使用 URL 路径值来指定 Redis 实例。我使用 Docker 在本地运行 Redis 实例。你可以使用任何工具运行 Redis 实例,只需确保替换相应的 URL 即可。
res, err := limiter.Allow(ctx, clientIP, redis_rate.PerMinute(10))
全屏模式,然后退出全屏
它接受三个参数,第一个是 ctx
,第二个是 Key,即 Redis 数据库中的键(用于一个值的键),第三个是限制。因此,该函数会将 clientIP 地址作为键,将默认限制作为值存储,并在每次请求时减少计数。这种结构的原因在于 Redis 数据库需要唯一的标识符和唯一的键来存储键值对形式的数据,而每个 IP 地址都是唯一的,这就是为什么我们使用 IP 地址而不是用户名等。可以修改第三个参数redis_rate.PerMinute(10)
,我们可以设置每秒、每分钟或每小时的请求限制,并在括号内指定允许的请求数量。在我们的情况下,它是每分钟10次。是的,设置起来就这么简单。
最后,我们检查是否有剩余配额,通过 res.Remaining
。如果为零,我们将返回一个带有消息的错误信息,否则返回 nil(空)。你可以通过 res.Limit.Rate
查询限制速率,等等。你可以自由探索,进一步挖掘更多。需要注意的是,这只是如何结合这两部分的一个例子。由于只有一个路由,我们没有使用任何中间件。如果有数十或数百个路由会怎样呢?
我们现在开始看 main()
函数。
func main() {
r := gin.Default()
r.GET("/message", func(c *gin.Context) {
err := rateLimiter(c.ClientIP())
if err != nil {
c.JSON(http.StatusTooManyRequests, gin.H{
"message": "您已达上限",
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "您可以继续请求",
})
})
r.Run(":8081")
}
全屏模式,退出全屏
基本上一切都一样。每次有人访问这个路由时,我们都会调用 rateLimit()
函数,并传入 ClientIP 地址,将返回的错误存储在 err
变量中。如果有错误,我们将返回错误代码 429,即 http.StatusTooManyRequests
,并附带消息 "message": "您已达到限制"
。如果用户还有剩余的请求次数,并且 rateLimit()
无错返回,则一切正常,会处理请求。
解释到此为止。我们现在来测试一下工作。通过重新运行服务器并执行相同的命令。第一次刷新时,你会看到之前得到的相同消息。现在刷新你的浏览器10次(因为我们设定每分钟最多刷新10次),你会在浏览器中看到错误信息。
我们也可以通过查看终端的日志来验证。Gin 自带很好的日志记录功能。大约一分钟之后,它会自动恢复我们的配额。
这篇文章到这里就结束了,希望你喜欢这篇文章和我写它一样多。很高兴你能读到这儿——非常感谢你的支持,真的。我经常在X (推特)上聊Golang和其他话题,如开源项目和Docker。你也可以在那儿联系我。