GO 流量限制之令牌桶的实现

0x00 流量限制的手段

流量限制的手段有很多,最常见的:漏桶、令牌桶两种:

  1. 漏桶是指我们有一个一直装满了水的桶,每过固定的一段时间即向外漏一滴水。如果你接到了这滴水,那么你就可以继续服务请求,如果没有接到,那么就需要等待下一滴水。
  2. 令牌桶则是指匀速向桶中添加令牌,服务请求时需要从桶中获取令牌,令牌的数目可以按照需要消耗的资源进行相应的调整。如果没有令牌,可以选择等待,或者放弃。

这两种方法看起来很像,不过还是有区别的。漏桶流出的速率固定,而令牌桶只要在桶中有令牌,那就可以拿。也就是说令牌桶是允许一定程度的并发的,比如同一个时刻,有100个用户请求,只要令牌桶中有100个令牌,那么这100个请求全都会放过去。令牌桶在桶中没有令牌的情况下也会退化为漏桶模型。

0x01 简单令牌桶的实现

简单令牌桶的实现原理是使用定时器,每隔一定时间间隔向桶内放入令牌,实现如下:


// GetBucket return a token bucket
// capacityPs is maximum concurrency in one second
// capacityPs is maximum bucket capacity
func GetBucket(capacityPs, maxCapacity int) chan struct{} {
	var bucketToken = make(chan struct{}, maxCapacity)
	timeD := time.Second / time.Duration(capacityPs)
	ticker := time.NewTicker(timeD)
	go func() {
		for {
			select {
			case <-ticker.C:
				select {
				case bucketToken <- struct{}{}:
				default:
				}
			}
			fmt.Println(len(bucketToken), time.Now())
		}
	}()
	return bucketToken
}

有新的请求时需要获取token,获取token时可以选择阻塞或者非阻塞两种方式,获取token的实现方式如下:

func GetToken(block bool, bucket *chan struct{}) bool {
	if block {
		select {
		case <-*bucket:
			return true
		}
	} else {
		select {
		case <-*bucket:
			return true
		default:
			return false
		}
	}
}

整体方法的使用方式如下:

package main

import (
	"fmt"
	"time"
)

// GetBucket return a token bucket
// capacityPs is maximum concurrency in one second
// capacityPs is maximum bucket capacity
func GetBucket(capacityPs, maxCapacity int) chan struct{} {
	var bucketToken = make(chan struct{}, maxCapacity)
	timeD := time.Second / time.Duration(capacityPs)
	ticker := time.NewTicker(timeD)
	go func() {
		for {
			select {
			case <-ticker.C:
				select {
				case bucketToken <- struct{}{}:
				default:
				}
			}
			fmt.Println(len(bucketToken), time.Now())
		}
	}()
	return bucketToken
}

func GetToken(block bool, bucket *chan struct{}) bool {
	if block {
		select {
		case <-*bucket:
			return true
		}
	} else {
		select {
		case <-*bucket:
			return true
		default:
			return false
		}
	}
}

func main() {

	capacityPs := 50
	maxCapacity := 100
	bucket := GetBucket(capacityPs, maxCapacity)
	for {
		time.Sleep(100 * time.Millisecond)
		go fmt.Println("====", time.Now(), GetToken(false, &bucket))
		go fmt.Println("====", time.Now(), GetToken(false, &bucket))
		go fmt.Println("====", time.Now(), GetToken(false, &bucket))
		go fmt.Println("====", time.Now(), GetToken(false, &bucket))
		go fmt.Println("====", time.Now(), GetToken(false, &bucket))
	}

}

0x02 github.com/juju/ratelimit

取消定时填充token逻辑,改为记录上次填充时间,通过计算距今时间差值来计算该时间段内应填充的数量,一次性进行填充。

记:

  • 桶中剩余的token数量为:cnt
  • token数量上限为: cap
  • 每次填充的时间间隔为:d
  • 每次填充的token数为: q
  • 上次填充的时间为:t1
  • 当前时间为: t2

则在 t2 时刻桶中的token数量应该为:

min( cnt + ((t2 - t1) / d) * q , cap )

0x03 github.com/uber-go/ratelimit

  • TODO

没太看懂,有时间再看

0x04 参考文档

本文参考了《Go语言高级编程》,作者:柴树杉、曹春晖。

发布了88 篇原创文章 · 获赞 90 · 访问量 29万+

猜你喜欢

转载自blog.csdn.net/leiflyy/article/details/104951339
今日推荐