Documentation
¶
Overview ¶
Package ratelimit 提供通用限流器实现。
支持令牌桶和滑动窗口两种限流算法,以及按 key 隔离的限流器管理; Lockout 提供按 key 的防爆破锁(fail2ban 形态:失败计数达阈值 → 锁定拒绝态)。
用法:
// 令牌桶限流器
limiter := ratelimit.NewTokenBucket(100, 200) // 100 QPS, burst 200
if limiter.Allow() {
// 处理请求
}
// 按 key 隔离的限流器(如按用户 ID)
keyed := ratelimit.NewKeyedLimiter(func() ratelimit.Limiter {
return ratelimit.NewTokenBucket(10, 20)
}, 10*time.Minute)
if keyed.Allow("user:123") {
// 处理请求
}
// 防爆破锁(登录/API Key 防爆破、匿名接口防枚举)
lo := ratelimit.NewLockout(ratelimit.LockoutConfig{
Window: 10 * time.Minute, Threshold: 5, LockDuration: 15 * time.Minute,
})
if locked, remain := lo.CheckLocked("user:42"); locked {
// 对外 429 + 剩余秒数文案(remain 即剩余锁定时长)
}
if locked, _ := lo.RecordFailure("user:42"); locked {
// 本次失败触发锁定(或已在锁定中)
}
lo.Reset("user:42") // 登录成功清零
Index ¶
- type KeyedLimiter
- type KeyedLimiterOf
- func (kl *KeyedLimiterOf[K]) Allow(key K) bool
- func (kl *KeyedLimiterOf[K]) AllowN(key K, n int) bool
- func (kl *KeyedLimiterOf[K]) AllowWithRetryAfter(key K) (wait time.Duration, ok bool)
- func (kl *KeyedLimiterOf[K]) Close()
- func (kl *KeyedLimiterOf[K]) Create(key K, l Limiter) bool
- func (kl *KeyedLimiterOf[K]) Delete(key K)
- func (kl *KeyedLimiterOf[K]) Get(key K) Limiter
- func (kl *KeyedLimiterOf[K]) Len() int
- func (kl *KeyedLimiterOf[K]) OverCapHits() int64
- func (kl *KeyedLimiterOf[K]) Replace(key K, l Limiter)
- type KeyedOption
- type Limiter
- type Lockout
- type LockoutConfig
- type LockoutOf
- func (l *LockoutOf[K]) CheckLocked(key K) (locked bool, lockRemain time.Duration)
- func (l *LockoutOf[K]) Close()
- func (l *LockoutOf[K]) Len() int
- func (l *LockoutOf[K]) OverCapHits() int64
- func (l *LockoutOf[K]) RecordFailure(key K) (locked bool, lockRemain time.Duration)
- func (l *LockoutOf[K]) Reset(key K)
- type SlidingWindow
- type TokenBucket
- type TokenBucketOption
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type KeyedLimiter ¶
type KeyedLimiter = KeyedLimiterOf[string]
KeyedLimiter 是 string key 的别名,兼容既有调用方。
func NewKeyedLimiter ¶
func NewKeyedLimiter(factory func() Limiter, ttl time.Duration) *KeyedLimiter
NewKeyedLimiter 创建 string key 的限流器管理器(兼容旧 API)。
参数:
- factory: 创建新限流器的工厂函数
- ttl: 限流器空闲过期时间,超过此时间未使用的限流器会被清理
用法:
keyed := ratelimit.NewKeyedLimiter(func() ratelimit.Limiter {
return ratelimit.NewTokenBucket(10, 20)
}, 10*time.Minute)
type KeyedLimiterOf ¶ added in v0.5.0
type KeyedLimiterOf[K comparable] struct { // contains filtered or unexported fields }
KeyedLimiterOf 是按 key 隔离的限流器管理器(key 类型可定制)。 每个 key 维护独立的限流器实例,支持空闲回收、容量护栏、速率热更。
func NewKeyedLimiterByFunc ¶ added in v0.16.0
func NewKeyedLimiterByFunc[K comparable](factory func(K) Limiter, opts ...KeyedOption[K]) *KeyedLimiterOf[K]
NewKeyedLimiterByFunc 创建按 key 隔离、且**配额由 key 决定**的限流器管理器(#31)。
NewKeyedLimiterOf 的 factory 签名是 func() Limiter —— 不接受 key,因此一个管理器内 所有 key 只能共享同一份配额。业务上按接口维度分档位(同步类宽松 / 备份类严格 / 导入确认最严)是常见形态,此前只能把限流器实例的键从 (maxReq, window) 扩成 (action, maxReq, window),即每个 action 一个管理器 —— 那样按 key 隔离、空闲回收、 容量护栏、Len()/OverCapHits() 可观测出口这些价值基本被绕过,整体限流状态也没有 单一治理点。本构造函数是它的正解:一个管理器,按 key 定配额。
用法:
quota := map[string]struct {
maxReq int
window time.Duration
}{
"backup/import/confirm": {2, 900 * time.Second},
"car/syncAllTripData": {3, 300 * time.Second},
}
keyed := ratelimit.NewKeyedLimiterByFunc[string](func(k string) ratelimit.Limiter {
q := quota[k]
return ratelimit.NewSlidingWindow(q.window, q.maxReq)
}, ratelimit.WithIdleTTL[string](10*time.Minute))
ok := keyed.Allow("car/syncAllTripData") // 该 key 用自己那档配额
⚠️ 未在配额表里登记的 key 会拿到 factory 返回的值(若 factory 写了兜底默认值, 就是静默按兜底档限流)。这是 API 形状使然,不是库的 bug——但它曾导致一次迁移中 未登记的接口被按默认档限流。**配额表缺 key 时应让它显式失败**(返回 nil 会让 Allow 返回 false,见 Get 的容量护栏说明),或在 Allow 处自行判空,避免静默降级。
func NewKeyedLimiterOf ¶ added in v0.5.0
func NewKeyedLimiterOf[K comparable](factory func() Limiter, opts ...KeyedOption[K]) *KeyedLimiterOf[K]
NewKeyedLimiterOf 创建按 key 隔离的限流器管理器(key 任意 comparable 类型), 所有 key 共用同一份限流配额。
需要**按 key 给不同配额**(不同接口不同档位)请用 NewKeyedLimiterByFunc(#31)。
用法:
keyed := ratelimit.NewKeyedLimiterOf[uint](
func() ratelimit.Limiter { return ratelimit.NewTokenBucket(10, 20) },
ratelimit.WithIdleTTL[uint](10*time.Minute),
ratelimit.WithMaxEntries[uint](10_000),
)
func (*KeyedLimiterOf[K]) Allow ¶ added in v0.5.0
func (kl *KeyedLimiterOf[K]) Allow(key K) bool
Allow 检查指定 key 是否允许请求。容量护栏拒绝未登记 key 时返回 false。
func (*KeyedLimiterOf[K]) AllowN ¶ added in v0.5.0
func (kl *KeyedLimiterOf[K]) AllowN(key K, n int) bool
AllowN 检查指定 key 是否允许 N 个请求。容量护栏拒绝未登记 key 时返回 false。
func (*KeyedLimiterOf[K]) AllowWithRetryAfter ¶ added in v0.5.0
func (kl *KeyedLimiterOf[K]) AllowWithRetryAfter(key K) (wait time.Duration, ok bool)
AllowWithRetryAfter 检查是否放行;拒绝时返回建议等待时长(不消耗令牌的探测)。
- ok=true:请求放行,wait=0
- ok=false:请求拒绝;wait 是下一个可用令牌的等待时长(Reserve→Delay→Cancel, 不消耗令牌),容量护栏拒绝时返回建议 Retry-After
探测语义:被拒请求不会自我消耗配额,避免放大限流(429 + Retry-After 场景)。
func (*KeyedLimiterOf[K]) Close ¶ added in v0.5.0
func (kl *KeyedLimiterOf[K]) Close()
Close 关闭限流器管理器,停止清理 goroutine。可安全重复调用。
func (*KeyedLimiterOf[K]) Create ¶ added in v0.17.0
func (kl *KeyedLimiterOf[K]) Create(key K, l Limiter) bool
Create 在容量护栏内原子登记新 key 的限流器([#33](https://github.com/oekit/concurrency/issues/33)): 表满先回收空闲条目(与 Allow 建桶路径同一 sweep-on-pressure),仍满返回 false 并计一次 OverCapHits。已登记的 key 返回 false 且**不覆盖**——速率热更请走 Replace(换桶不该被 护栏卡住)。l 为 nil 返回 false。
与 NewKeyedLimiterByFunc 互补:ByFunc 的 factory 在库内锁内被调用,消费方若在 factory 里读自持锁的速率状态会形成「库锁→消费方锁」与正常路径「消费方锁→库锁」的 锁序倒置;「每次调用传入当前配置速率」的消费方(Nacos/DB 热更即时生效)应外部构造 桶后用 Create 登记 / Replace 热更,配额回收与护栏都留在库内。
func (*KeyedLimiterOf[K]) Delete ¶ added in v0.5.0
func (kl *KeyedLimiterOf[K]) Delete(key K)
Delete 删除指定 key 的限流器。
func (*KeyedLimiterOf[K]) Get ¶ added in v0.5.0
func (kl *KeyedLimiterOf[K]) Get(key K) Limiter
Get 获取指定 key 的限流器。表满且 key 未登记时返回 nil。
func (*KeyedLimiterOf[K]) Len ¶ added in v0.5.0
func (kl *KeyedLimiterOf[K]) Len() int
Len 返回当前活跃的限流器数量。
func (*KeyedLimiterOf[K]) OverCapHits ¶ added in v0.5.0
func (kl *KeyedLimiterOf[K]) OverCapHits() int64
OverCapHits 返回容量护栏拒绝未登记 key 的累计次数(可观测性)。
func (*KeyedLimiterOf[K]) Replace ¶ added in v0.5.0
func (kl *KeyedLimiterOf[K]) Replace(key K, l Limiter)
Replace 将指定 key 的限流器替换为新实例(速率热更)。 换桶即新配额,桶内余量归零(调用方接受)。key 不存在时等价登记新桶。
type KeyedOption ¶ added in v0.5.0
type KeyedOption[K comparable] func(*keyedConfig[K])
KeyedOption 是 KeyedLimiterOf 的函数选项。
func WithIdleTTL ¶ added in v0.5.0
func WithIdleTTL[K comparable](ttl time.Duration) KeyedOption[K]
WithIdleTTL 设置空闲回收周期;表满时也先触发回收再判满。 <=0 使用默认 1 分钟。
小于 2ns 的值不会让后台清扫 panic——清扫节奏有 1s 下限(见 cleanupInterval), 过期判定仍按本值走。
func WithMaxEntries ¶ added in v0.5.0
func WithMaxEntries[K comparable](max int) KeyedOption[K]
WithMaxEntries 设置容量护栏上限(未登记 key 在表满时被拒绝,已登记 key 不受影响)。 0 = 不限(默认)。
func WithOverCapRetryAfter ¶ added in v0.5.0
func WithOverCapRetryAfter[K comparable](d time.Duration) KeyedOption[K]
WithOverCapRetryAfter 设置容量护栏拒绝时 AllowWithRetryAfter 返回的建议等待。 <=0 使用默认 1 分钟。
type Limiter ¶
type Limiter interface {
// Allow 检查是否允许一个请求。
Allow() bool
// AllowN 检查是否允许 N 个请求。
AllowN(n int) bool
// Reserve 预留一个令牌,返回需要等待的时间。
// 如果返回 0 表示立即可用。
Reserve() time.Duration
}
Limiter 是通用限流器接口。
type Lockout ¶ added in v0.11.0
Lockout 是 string key 的别名,兼容惯例(对齐 KeyedLimiter)。
func NewLockout ¶ added in v0.11.0
func NewLockout(cfg LockoutConfig) *Lockout
NewLockout 创建 string key 的防爆破锁。 Window/Threshold/LockDuration 为必填语义参数,非法值直接 panic (配置错误宁可启动即暴露——锁静默失效等于没有防护)。
type LockoutConfig ¶ added in v0.11.0
type LockoutConfig struct {
// Window 为失败计数窗口:窗口内的失败事件按 key 累计(滑动窗口,
// 半开区间 (now-Window, now],窗口外样本自然老化)。
Window time.Duration
// Threshold 为锁定阈值:窗口内第 N 次失败触发锁定。
Threshold int
// LockDuration 为锁定时长:触发后 key 进入拒绝态持续 L 秒,期间 CheckLocked
// 返回剩余时长(供对外 429 文案);到期自动解除。后续失败不延长锁定。
LockDuration time.Duration
// MaxEntries 为容量护栏上限(0 = 不限,对齐 KeyedLimiterOf.WithMaxEntries)。
// 表满且 key 未登记时失败样本被丢弃(fail-open + OverCapHits 观测)——
// 不因内部容量问题拒绝认证尝试(那会放大成对合法用户的拒绝服务)。
MaxEntries int
// IdleTTL 为空闲回收周期;<=0 用默认 1 分钟。锁定中的条目免于回收
// (提前驱逐等于提前解锁);窗口内仍有有效失败样本的条目同样免于回收
// (#83:空闲回收线不得短于失败窗口,否则滑窗语义被截短,阈值可被
// 「间隔 > IdleTTL」的分布式攻击绕过);回收不改变锁定到期语义。
// 小于 2ns 的值不会让后台清扫 panic——清扫节奏有 1s 下限(见 cleanupInterval),
// 空闲判定仍按本值走。
IdleTTL time.Duration
// Now 为时钟注入(nil = time.Now;与 budget.WithClock 同款,
// 假时钟测试完全确定)。清扫节奏仍走真实 ticker,到期/空闲判定一律用本时钟。
Now func() time.Time
}
LockoutConfig 是 Lockout 的构造配置。
type LockoutOf ¶ added in v0.11.0
type LockoutOf[K comparable] struct { // contains filtered or unexported fields }
LockoutOf 是按 key 隔离的防爆破锁(fail2ban 形态):窗口内失败事件达阈值 → key 进入锁定拒绝态持续 LockDuration(期间即使凭证正确也应拒绝)→ 成功操作 Reset 清零。与家族原语的边界:KeyedLimiter 是速率型令牌桶(表达不了 「计数达标后进入持续拒绝态」);breaker 是调用方视角的实例级熔断(无 key 隔离); budget 是超限记账存档(无拒绝态与 Reset)。
语义细则:
- 失败样本滑动窗口计数,窗口外自然老化——锁定到期后若旧失败仍在窗口内, 首个新失败即重新锁定(重复违规者语义,对齐 fail2ban findtime 行为);
- 锁定期间新失败只入窗口记账(封顶 Threshold 个),不延长当前锁定;
- 容量护栏 fail-open:表满且 key 未登记时丢弃失败样本并计入 OverCapHits;
- 锁定中的条目免于空闲回收(提前驱逐等于提前解锁);窗口内仍有有效失败 样本的条目同样免于回收——空闲回收只清理「已无窗口内样本」的条目, 否则窗口语义会被 IdleTTL 截短(#83)。
典型用法(登录防爆破):
lo := ratelimit.NewLockout(ratelimit.LockoutConfig{
Window: 10 * time.Minute, Threshold: 5, LockDuration: 15 * time.Minute,
MaxEntries: 10_000,
})
if locked, remain := lo.CheckLocked("user:42"); locked {
return &LockedError{RemainingSeconds: int(remain.Seconds())} // 对外 429
}
if err := verifyCredential(); err != nil {
if locked, _ := lo.RecordFailure("user:42"); locked { ... }
return err
}
lo.Reset("user:42") // 登录成功
func NewLockoutOf ¶ added in v0.11.0
func NewLockoutOf[K comparable](cfg LockoutConfig) *LockoutOf[K]
NewLockoutOf 创建任意 comparable key 的防爆破锁(IP、用户名、API Key 等)。
func (*LockoutOf[K]) CheckLocked ¶ added in v0.11.0
CheckLocked 查询 key 是否处于锁定拒绝态:locked=true 时 lockRemain 为剩余 锁定时长(供对外 429 文案),未锁定或未登记返回 (false, 0)。查询刷新空闲计时。 返回序与 RecordFailure 统一为 (locked, lockRemain)——两个方法同序是刻意设计, 避免调用方在相邻调用间交换返回值顺序。
func (*LockoutOf[K]) Close ¶ added in v0.11.0
func (l *LockoutOf[K]) Close()
Close 停止后台清扫 goroutine。可安全重复调用(KeyedLimiterOf 同款幂等语义)。
func (*LockoutOf[K]) OverCapHits ¶ added in v0.11.0
OverCapHits 返回容量护栏丢弃失败样本的累计次数(可观测性)。
func (*LockoutOf[K]) RecordFailure ¶ added in v0.11.0
RecordFailure 记一次失败事件。
- 触发锁定或 key 已处于锁定态:返回 (true, 剩余锁定时长);
- 未触发:返回 (false, 0);
- 容量护栏拒绝(表满且 key 未登记):失败样本被丢弃,返回 (false, 0), 可经 OverCapHits 观测。
锁定期间调用只入窗口记账,不延长当前锁定。
type SlidingWindow ¶
type SlidingWindow struct {
// contains filtered or unexported fields
}
SlidingWindow 是滑动窗口限流器。 在指定时间窗口内限制请求次数。
func NewSlidingWindow ¶
func NewSlidingWindow(window time.Duration, maxCount int) *SlidingWindow
NewSlidingWindow 创建滑动窗口限流器。
参数:
- window: 时间窗口大小
- maxCount: 窗口内最大请求数
用法:
limiter := ratelimit.NewSlidingWindow(time.Minute, 100) // 每分钟最多 100 次
func (*SlidingWindow) AllowN ¶
func (sw *SlidingWindow) AllowN(n int) bool
AllowN 检查是否允许 N 个请求。n<=0 视为 0 个请求,直接放行(不占槽位)。
func (*SlidingWindow) Reserve ¶
func (sw *SlidingWindow) Reserve() time.Duration
Reserve 预留一个请求,返回需要等待的时间。 返回 0 表示立即放行(槽位已计入窗口);返回 d>0 表示需等待 d 后放行, 槽位已按 now+d 预占——与 x/time/rate 的 Reserve 语义一致:预留即消费, 并发调用者拿到的是互不相同的槽位而非同一个"虚拟许可"。
type TokenBucket ¶
type TokenBucket struct {
// contains filtered or unexported fields
}
TokenBucket 是令牌桶限流器。 基于 golang.org/x/time/rate 实现,支持平滑限流;时间源可注入 WithTokenClock (缺省 time.Now),确定性测试注入假时钟即可无 sleep 驱动配额恢复与 Retry-After 探测(#24)。
func NewTokenBucket ¶
func NewTokenBucket(rps rate.Limit, burst int, opts ...TokenBucketOption) *TokenBucket
NewTokenBucket 创建令牌桶限流器。
参数:
- rps: 每秒允许的请求数(Requests Per Second)
- burst: 突发容量(令牌桶最大容量)
用法:
limiter := ratelimit.NewTokenBucket(100, 200) // 100 QPS, burst 200
func (*TokenBucket) Reserve ¶
func (tb *TokenBucket) Reserve() time.Duration
Reserve 预留一个令牌,返回需要等待的时间。
func (*TokenBucket) RetryAfter ¶ added in v0.5.0
func (tb *TokenBucket) RetryAfter() (time.Duration, bool)
RetryAfter 探测下一个可用令牌的等待时长,不消耗令牌(Reserve→Delay→Cancel, 归还按同一注入时钟结算)。ok=false 表示限流器本身不可用(如 burst=0 且 rps=0)。 被拒请求不会自我消耗配额——429 + Retry-After 场景的标准姿势。
type TokenBucketOption ¶ added in v0.12.0
type TokenBucketOption func(*TokenBucket)
TokenBucketOption 用于定制 TokenBucket(函数选项模式;按本仓命名硬规则为 XxxOption,禁止裸 Option)。
func WithTokenClock ¶ added in v0.12.0
func WithTokenClock(clock func() time.Time) TokenBucketOption
WithTokenClock 注入时钟(缺省 time.Now;nil 视为缺省)。放行判定、预留与 Retry-After 探测全部走该时钟——假时钟下「1 秒后应恢复配额」「Retry-After 精确回填」均可确定性地断言,无 sleep 无 flake。