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>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user