package ratelimit import ( "log" "sync" "time" "github.com/redis/go-redis/v9" "golang.org/x/time/rate" ) // fallbackStore:redis 为主、内存为影子。每次调用先试 redis,出错即降级到 // 对应的内存实现(fail-open 到内存而非 fail-closed——redis 挂掉时防护降级但 // 不中断,业务请求不 5xx)。redis 恢复后自动回主路径。降级日志限频打印。 type fallbackStore struct { redis *redisStore mem *memoryStore } func newFallbackStore(rdb *redis.Client) *fallbackStore { return &fallbackStore{redis: newRedisStore(rdb), mem: newMemoryStore()} } // warnDegraded 限频告警(每 30s 至多一条,防 redis 宕机刷爆日志)。 var ( warnMu sync.Mutex warnLast time.Time ) func warnDegraded(err error) { warnMu.Lock() defer warnMu.Unlock() if time.Since(warnLast) < 30*time.Second { return } warnLast = time.Now() log.Printf("[ratelimit] redis 不可用,已降级内存限流:%v", err) } func (s *fallbackStore) NewLimiter(r rate.Limit, burst int) Limiter { return &fallbackLimiter{r: s.redis.newLimiter(r, burst), m: s.mem.NewLimiter(r, burst)} } func (s *fallbackStore) Counter() Counter { return &fallbackCounter{r: &redisCounter{rdb: s.redis.rdb}, m: s.mem.Counter()} } func (s *fallbackStore) FailLocker() FailLocker { return &fallbackFailLocker{r: &redisFailLocker{rdb: s.redis.rdb}, m: s.mem.FailLocker()} } type fallbackLimiter struct { r *redisLimiter m Limiter } func (l *fallbackLimiter) Allow(key string) Result { res, err := l.r.allow(key) if err != nil { warnDegraded(err) return l.m.Allow(key) } return res } type fallbackCounter struct { r *redisCounter m Counter } func (c *fallbackCounter) Incr(key string, ttl time.Duration) (int64, error) { n, err := c.r.Incr(key, ttl) if err != nil { warnDegraded(err) return c.m.Incr(key, ttl) } return n, nil } type fallbackFailLocker struct { r *redisFailLocker m FailLocker } func (l *fallbackFailLocker) Locked(key string) bool { locked, err := l.r.locked(key) if err != nil { warnDegraded(err) return l.m.Locked(key) } return locked } func (l *fallbackFailLocker) RecordFailure(key string, max int, lockFor time.Duration) { if err := l.r.recordFailure(key, max, lockFor); err != nil { warnDegraded(err) l.m.RecordFailure(key, max, lockFor) } } func (l *fallbackFailLocker) Reset(key string) { // 双清:降级期间可能在内存里积了计数,恢复后一并清掉。 if err := l.r.reset(key); err != nil { warnDegraded(err) } l.m.Reset(key) }