12--限流中间件
当前系统的问题:
- 一个用户可以对同一个秒杀活动无限次抢购
- 恶意用户可以高频访问接口耗尽资源
- Redis Lua 只保证库存不超卖,不限制用户请求次数
设计方案:Redis + 滑动窗口算法
在 Redis 中记录每个用户在所有秒杀活动下的访问次数,超过限制则拒绝。
Key 格式:ratelimit:user:{userId}
Value:访问次数
TTL:1 秒(每秒清零)
一. 创建限流中间件
internal/middleware/ratelimit.go
package middleware
import (
"fmt"
"net/http"
"time"
"github.com/Chuan81/secgo-mall/internal/cache"
"github.com/gin-gonic/gin"
)
// RateLimit 限流中间件
// 使用 Redis INCR + EXPIRE 实现滑动窗口限流
// 参数 limit: 每秒最多允许的请求次数
func RateLimit(limit int) gin.HandlerFunc {
return func(c *gin.Context) {
// 从JWT上下文中获取用户ID
uid, exists := c.Get("uid")
if !exists {
// uid不存在时直接放行(JWTAuth会在后续进行拦截)
c.Next()
return
}
// 生成Redis Key: ratelimit:user:{uid}
// 每个用户对所有秒杀活动都有一个独立的限流计数器
key := fmt.Sprintf("ratelimit:user:%v", uid)
// 原子递增 INCR(避免多并发竞争)
count, err := cache.Rdb.Incr(c.Request.Context(), key).Result()
if err != nil {
// Redis报错时fail open(放行请求,不阻塞服务)
c.Next()
return
}
// 第一次访问设置TTL为1秒(每秒清零)
if count == 1 {
cache.Rdb.Expire(c.Request.Context(), key, time.Second)
}
// 超过限制,拦截请求
if count > int64(limit) {
// 超出限流阈
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
"code": 429,
"message": "too many requests, please try again later",
})
return
}
c.Next()
}
}
二. 应用限流中间件
我们只需要修改cmd/secgo-mall/main.go中的/seckill/purchase路由
// 下单
// 抢购接口限流:每秒最多5次
protected.POST("/seckill/purchase", middleware.RateLimit(5), handlers.SeckillPurchase)
注意:限流中间件要放在
JWT之后,因为需要从 Context 获取uid。
三. 测试
# 准备购买请求文件(Windows 下用 -D 传 body)
echo {"seckill_id":8} > purchase.json
# 获取用户有效 token
# 第 6 次开始被限流
hey -n 6 -c 6 -m POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <USER_TOKEN>" \
-D purchase.json \
http://localhost:8080/api/seckill/purchase
# 预期结果:返回 429 rate limit exceeded
可以看到6次并发请求中,5次被同意,一次被429


浙公网安备 33010602011771号