package stocklock import ( "context" "fmt" "sync/atomic" "time" "github.com/redis/go-redis/v9" ) // 滑动窗口:ZSET 里存每次请求的时间戳(毫秒),先删掉窗口外的,再数窗口内的 var slidingScript = redis.NewScript(` local key, now, window, limit, member = KEYS[1], tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3]), ARGV[4] redis.call("ZREMRANGEBYSCORE", key, 0, now - window) if redis.call("ZCARD", key) >= limit then return 0 end redis.call("ZADD", key, now, member) redis.call("PEXPIRE", key, window) return 1`) type SlidingWindow struct { rdb *redis.Client limit int window time.Duration seq func() string // 生成唯一成员,避免同一毫秒的请求互相覆盖 } func NewSlidingWindow(rdb *redis.Client, limit int, window time.Duration) *SlidingWindow { var n atomic.Uint64 // 并发安全的序号;多实例部署时成员里再加上实例标识或用 UUID return &SlidingWindow{rdb: rdb, limit: limit, window: window, seq: func() string { return fmt.Sprintf("%d-%d", time.Now().UnixNano(), n.Add(1)) }} } // Allow 判断 key(如 "rl:user:42")在当前窗口内是否还能通过 func (s *SlidingWindow) Allow(ctx context.Context, key string, now time.Time) (bool, error) { n, err := slidingScript.Run(ctx, s.rdb, []string{key}, now.UnixMilli(), s.window.Milliseconds(), s.limit, s.seq()).Int() return n == 1, err }