分布式限流的实现

系统的运行过程中,需要对外部请求进行限流。限流有本地限流与分布式限流,本文现在对项目实践过程中使用的分布式限流中间件进行介绍。
该分布式限流中间件实现的原理是每次从远端(远端的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操作来申请配额,等到超过限定后,进行限流。

你可能感兴趣的:(架构)