系统的运行过程中,需要对外部请求进行限流。限流有本地限流与分布式限流,本文现在对项目实践过程中使用的分布式限流中间件进行介绍。
该分布式限流中间件实现的原理是每次从远端(远端的redis)消费掉固定数量的配额,待消费完后,再从远端申请。
// rate.go
// 定义了限流的频次
type Rate struct {
Period int64 // 秒级别
Limit int64
}
核心实现:
// ratelimit.go
func NewRateLimit(name string, rate Rate, quota int64, ops ...Option) (*RateLimit, error) {
// quota 每次申请的配额
// quota 不应该过大
if rate.Limit <= quota*10 { .... 生成错误}
rl := &RateLimit{
name : name,
quota : quota,
rate : rate,
// 消费剩下的
remainder : 0,
// 是否超出限定
over : false
// 记录时间
tick : time.Now().Unix()
}
// rl.spinlock : if more request call remote meanwhile, only one pass
rl.spinlock.store(false)
if len(ops) > 0 {
for _, opt := range opts {
opt(rl)
}
}
if rl.client == nil {
... 生成一个redis client
}
return rl, nil
}
func (rl *RateLimit) Allow(token int64) bool {
if toke <= 0 { return false}
var span time.Duration
for rl.spinlock.load().(bool) {
// 此时,已有请求在请求远端
time.Sleep(DefaultWaitInterval)
span += DefaultWaitInterval
if span >= MaxWaitInterval {
// 降级策略,避免过长时间的等待
logs.Warnf(...)
return true
}
}
now := time.Now().Unix() / rate.Period
if now == rl.tick() {
// 仍然在同一个时间段内
if rl.over() {
return false
}
// 为了性能的需要,忍受了可能的超卖
if rl.loadInt64(&rl.remainer) > token {
rl.AddInt64(&rl.remainer, ^int64(token - 1))
return true
}
}
// 从远端申请配额
return rl.consumeRemote(now, token)
}
从远端申请配额:
// redis.go
func (rl *RateLimte) consumeRemote( now int64, token int64) bool {
// 标记状态
rl.spinlock.store(true)
defer rl.spinlock.store(false)
// here adjust quota to larget tokens
quota := rl.quota
if quota < tokens {
quota = tokens
}
key := rl.getKey(now)
total, err := rl.client.IncrBy(key, quota).Result()
if total == quota {
// 第一次,设定过期时间
go rl.clent.Expire(key, time.Duration(rl.rate.Period)* time.Second * 2).Result()
}
if err != nil {
logs...
// 降级策略
return true
}
var more int64
if total <= rl.rate.Limit {
more = quota
} else {
more = quota + rl.rateLimit - total
}
if more < tokens {
rl.over = true
retur false
}
if rl.tick == now {
rl.AddInt64(&rl.remainer, more - tokens)
} else {
rl.StoreInt64(&rl.remainer, more - tokens)
rl.tick = now
rl.over = false
}
return true
}
func (*RateLimit) getKey(now int64) string {
return prefix + ":" + version + ":" + rl.name + ":" + strconv.FormatInt(now, 10)
}
在代码实现的过程中,为了性能的需要, 做了很多降级策略,同时避免了锁的使用,而是尽量使用原子操作。
因为公司的redis不支持lua, 所以在向远端请求的时候,使用redis的IncrBy操作来申请配额,等到超过限定后,进行限流。
1万+

被折叠的 条评论
为什么被折叠?



