Files
wangjia 3c687e5e0b feat(backend): 限流/登录失败锁状态外置 Redis(todo #2)
新包 internal/ratelimit:内存/redis(GCRA+Lua) 双实现 + 出错逐调用降级内存
(fail-open 到内存不 fail-closed)。REDIS_ADDR 空=内存模式,行为与既往一致;
配置后跨重启保状态、支持多实例。miniredis 全覆盖测试,零真实外部依赖。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 10:42:23 +08:00

107 lines
2.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package ratelimit
import (
"log"
"sync"
"time"
"github.com/redis/go-redis/v9"
"golang.org/x/time/rate"
)
// fallbackStoreredis 为主、内存为影子。每次调用先试 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)
}