Files

43 lines
1.4 KiB
Go

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
}