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

Ratelimit_test

posted @ 2026-04-17 17:46  Chuan81  阅读(7)  评论(0)    收藏  举报