限流算法的golang实现

限流算法

常见的限流算法有漏桶算法(Leaky Bucket),令牌桶算法(Token Bucket)

  • 漏桶算法(Leaky Bucket)
    原理类似于漏桶的进水和出水,流入漏桶的水流大于流出漏桶的水流时,漏桶就会慢慢变满,水满后就会溢出。请求访问服务,当突发请求大于服务承受数量时,服务器拒绝访问,服务器以一定的速度响应请求。

  • 令牌桶算法(Token Bucket)
    和漏桶算法不同的是,令牌桶算法以一定的速率向桶里放入token, 当桶满时就暂停加入token或者丢弃token。每个请求获得一个token才可以进行下一步,没有token就会阻塞或被拒绝。令牌桶允许突发流量,如突然将桶内令牌消耗完。

golang实现令牌桶算法


type Bucket struct {
	startTime time.Time
	// token桶的容量
	capacity int64
	// 时间间隔,
	fillInterval time.Duration
	// 每个时间间隔加入quantum个token
	quantum int64
	// 当前可用的token
	avail int64
	// 当前的时间间隔
	availTick int64

	mu sync.Mutex
}

func (tb *Bucket) adjust(now time.Time) (currentTick int64) {
	currentTick = int64(now.Sub(tb.startTime) / tb.fillInterval)

	// token已经满,直接返回
	if tb.avail >= tb.capacity {
		return
	}

	// 更新token,
	tb.avail += (currentTick - tb.availTick) * tb.quantum
	if tb.avail > tb.capacity {
		// token大于容量,溢出处理
		tb.avail = tb.capacity
	}

	// 保存当前时间间隔
	tb.availTick = currentTick
	return
}

func (tb *Bucket) takeAvailable(now time.Time, count int64) int64 {
	if count <= 0 {
		return 0
	}
	tb.mu.Lock()
	defer tb.mu.Unlock()

    // 调整当前时间间隔的token数
	tb.adjust(now)
	if tb.avail <= 0 {
		return 0
	}
	if count > tb.avail {
		count = tb.avail
	}

	// 消耗count个token
	tb.avail -= count
	return count
}

func (tb *Bucket) TakeAvailable(count int64) int64 {
	return tb.takeAvailable(time.Now(), count)
}


分布式限流

同一个微服务通常会部署多个实例,在多个实例之间做限流实际是分布式限流问题。从redis 2.6版本开始,内嵌了Lua解析器。可以利用Redis原子性执行脚本的特点来实现分布式限流。

package main

import (
	"errors"
	"fmt"
	"time"

	"github.com/garyburd/redigo/redis"
)

// INCR同时设置过期时间
var redisINCRAndExpireCmd = redis.NewScript(1, `
local current
current = redis.call("incr",KEYS[1])
if tonumber(current) == 1 then
    redis.call("expire",KEYS[1],ARGV[1])
end
return current
`)

type RedisPool struct {
	pool *redis.Pool
}

func (r *RedisPool) INCR(key string, seconds int) (int, error) {
	if key == "" {
		return 0, fmt.Errorf("INCR key参数不能为空")
	}

	c := r.pool.Get()
	defer c.Close()

	return redis.Int(redisINCRAndExpireCmd.Do(c, key, seconds))
}

func formatKey(keyPrefix string, count int, seconds int64) (key string, err error) {
	if seconds < 1 {
		err = errors.New("时间间隔小于1秒")
		return
	}
	key = fmt.Sprintf("%s:ratelimiter:%d:%d", keyPrefix, count, seconds)
	return key, nil
}

// 限制Duration时间内的访问量为count次, QPS
func IsRateLimitExceed(p *RedisPool, keyPrefix string, count int, duration time.Duration) (exceed bool, current int, err error) {
	seconds := int64(duration / time.Second)
	key, err := formatKey(keyPrefix, count, seconds)
	if err != nil {
		return false, 0, err
	}
	current, err = p.INCR(key, int(seconds))
	if err != nil {
		return false, 0, err
	}
	return current > count, current, nil
}

参考

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值