常见限流方法总结

前言

限流的原因就不多说了,一句话,为了防止服务被高QPS打垮。在听组内分享A服务的时候,看到大家讨论限流的策略,其中说到令牌桶,又说到了漏桶,之前只了解令牌桶的思想,漏桶没有怎么了解过,趁着这次把限流方法总结下,并且把实现细节一并了解,算是一个小的学习。本次主要说的限流方法包括简单限流、漏桶、令牌桶。


简单限流

说下我们限流的思路,请求的速度我们这里主要是以每秒处理请求数量QPS为准,所谓限流就是我的接口承若的QPS最高每秒处理1W条请求,当请求超过1W的时候,我们要把高于1W的请求给拒绝掉,只处理那其中1W条。我们先来看看简单的限流如何做呢?话不多说,看代码


// RateLimiter 限速器

type RateLimiter struct {
	rate      uint64 //限流的速率
	allowance uint64 //桶剩余的容量
	max       uint64 //桶最大容量
	unit      uint64 //一次请求的单元时间片
	lastCheck uint64 //上次调用的时间点
}

看一眼创建限速器的代码


// New 创建RateLimiter实例
func New(rate int, per time.Duration) *RateLimiter {
	nano := uint64(per)
	if nano < 1 {
		nano = uint64(time.Second)
	}
	if rate < 1 {
		rate = 1
	}
	return &RateLimiter{
		rate:      uint64(rate),
		allowance: uint64(rate) * nano,
		max:       uint64(rate) * nano,
		unit:      nano,
	<span class="nx">lastCheck</span><span class="p">:</span> <span class="nf">unixNano</span><span class="p">(),</span>
<span class="p">}</span>

}

分析下代码,函数传进来的rate参数是限定处理的最大请求速率(一个时间周期内),per指的是一个时间周期的长度,比如rate为100,per为2time.Second 则代表,我们的最大速率是2秒钟处理100次请求。nano是把传进来的per转成了int64的整数,看起来是精确到了纳秒级别。allowance初始化为ratenano好理解,因为刚开始桶是空的。

ok,我们来看这个限流器是如何使用


func main() {
// rate limit: simple
rl := simpleratelimit.New(10, time.Second)
<span class="k">for</span> <span class="nx">i</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="p">&lt;</span> <span class="mi">100</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span> <span class="p">{</span>
	<span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">"limit result: %v\n"</span><span class="p">,</span> <span class="nx">rl</span><span class="p">.</span><span class="nf">Limit</span><span class="p">())</span>
<span class="p">}</span>

}

很简单,我们创建一个每秒钟最高10次的限流器,然后接着调用限流器的Limit()函数,来判定本次请求是否需要被限制,返回true则拒绝,返回false则处理。ok,我们来看下Limit函数


// Limit 判断是否超过限制
func (rl *RateLimiter) Limit() bool {
now := unixNano()
<span class="nx">passed</span> <span class="o">:=</span> <span class="nx">now</span> <span class="o">-</span> <span class="nx">atomic</span><span class="p">.</span><span class="nf">SwapUint64</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">rl</span><span class="p">.</span><span class="nx">lastCheck</span><span class="p">,</span> <span class="nx">now</span><span class="p">)</span>

<span class="nx">rate</span> <span class="o">:=</span> <span class="nx">atomic</span><span class="p">.</span><span class="nf">LoadUint64</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">rl</span><span class="p">.</span><span class="nx">rate</span><span class="p">)</span>
<span class="nx">current</span> <span class="o">:=</span> <span class="nx">atomic</span><span class="p">.</span><span class="nf">AddUint64</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">rl</span><span class="p">.</span><span class="nx">allowance</span><span class="p">,</span> <span class="nx">passed</span><span class="o">*</span><span class="nx">rate</span><span class="p">)</span>

<span class="k">if</span> <span class="nx">max</span> <span class="o">:=</span> <span class="nx">atomic</span><span class="p">.</span><span class="nf">LoadUint64</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">rl</span><span class="p">.</span><span class="nx">max</span><span class="p">);</span> <span class="nx">current</span> <span class="p">&gt;</span> <span class="nx">max</span> <span class="p">{</span>
	<span class="nx">atomic</span><span class="p">.</span><span class="nf">AddUint64</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">rl</span><span class="p">.</span><span class="nx">allowance</span><span class="p">,</span> <span class="nx">max</span><span class="o">-</span><span class="nx">current</span><span class="p">)</span>
	<span class="nx">current</span> <span class="p">=</span> <span class="nx">max</span>
<span class="p">}</span>

<span class="k">if</span> <span class="nx">current</span> <span class="p">&lt;</span> <span class="nx">rl</span><span class="p">.</span><span class="nx">unit</span> <span class="p">{</span>
	<span class="k">return</span> <span class="kc">true</span>
<span class="p">}</span>

<span class="c1">// 没有超过限额

atomic.AddUint64(&rl.allowance, -rl.unit)
return false
}

分析下过程:

1.首先now为当前时间点,passed为当前时间减去上次请求的时间点,即为距离上次请求过去时间,注意SwapUint64顺带更新了lastCheck为当前时间。

2.接着读取下速率rate

3.然后计算current=passedrate,这里很有意思,刚开始还没看懂,实际上你理解下,passed是距离上次的时间,rate是速率,相乘代表什么?等会解释。

4.往下看有一个if判断,先读取桶的最大容量,然后比较current与max的大小,当大于max的时候,更新allowance,我们知道这时候实际上current是等于更新allowance的,更新的方式


atomic.AddUint64(&rl.allowance, max-current)

这不是把allowance更新为max了吗?然后把current=max。

到这里,基本已经明白了,上一步的

current := atomic.AddUint64(&rl.allowance, passedrate)

实际上在干什么,其实就是在往桶里放水的过程,这个水你可以认为是一段时间(实际时间乘以速率rate),current就是放进去这段时间之后的当前桶的容量。而这一步是为了判断放进去的水是否超过桶的最大容量,当超过了,则更正为max。

5.接下来,判断当前容量current是否低于rl.unit,rl.unit是什么,不就是一次请求需要的时间吗?如果当前容量根本不够一次的请求,必然需要给限流的,所以返回true

6.如果current够一次请求,则更显当前的配额,自然是减去一次请求的时间。

最后放出剩余的小部分代码


// UpdateRate 更新速率值
func (rl RateLimiter) UpdateRate(rate int) {
atomic.StoreUint64(&rl.rate, uint64(rate))
atomic.StoreUint64(&rl.max, uint64(rate)rl.unit)
}

// Undo 重置上一次调用Limit(),返回没有使用过的限额
func (rl *RateLimiter) Undo() {
current := atomic.AddUint64(&rl.allowance, rl.unit)

<span class="k">if</span> <span class="nx">max</span> <span class="o">:=</span> <span class="nx">atomic</span><span class="p">.</span><span class="nf">LoadUint64</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">rl</span><span class="p">.</span><span class="nx">max</span><span class="p">);</span> <span class="nx">current</span> <span class="p">&gt;</span> <span class="nx">max</span> <span class="p">{</span>
	<span class="nx">atomic</span><span class="p">.</span><span class="nf">AddUint64</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">rl</span><span class="p">.</span><span class="nx">allowance</span><span class="p">,</span> <span class="nx">max</span><span class="o">-</span><span class="nx">current</span><span class="p">)</span>
<span class="p">}</span>

}

// unixNano 当前时间(纳秒)
func unixNano() uint64 {
return uint64(time.Now().UnixNano())
}

UpdateRate当然是更新rate了,不多说。Undo比较有意思了,把最近上次调用使用的水又给放回去了,什么情况下会用到呢?假设你本次请求发现使用方调用的参数出错,实际上不占用你的下游QPS,你可以再把用的水放回去嘛。

接下来,我们思考下:

1.实际上这个实现方式的哲学思想是什么呢?我每次桶里放水是请求触发的,并不是我们异步的往桶里防水,会不会放水不及时呢?不会的,因为我们是先往桶里放水再判断是否限流的。

2.为什么要把max设置为我们的ratenano?我们不能把桶的最大容量设置的更大些吗?假设我把桶设置为100nano会出现什么问题呢?

当桶初始化的时候,桶的容量可以允许100次的请求,假设瞬间过来了100个请求,根据代码是不会限额的,好吧看来rate主要是在限制桶的最大容量来达到速率的限流。

3.你看Limit里面对限流器的操作并没有加锁,实际应用中我们的请求必然是高并发的,这种情况会出现问题吗?

我认为有问题,比如我们再放入水更新allowance的时候,


if max := atomic.LoadUint64(&rl.max); current > max {
atomic.AddUint64(&rl.allowance, max-current)
current = max
}

如果不做加锁,必然会可能造成两个协程先后判定if为true,接着先后都进入更新allowance,而两个都减去max-current会使得桶的容量小于max。

所以最好对桶的这块的操作加上锁,另外Undo的更新allowance同样需要,有必要对整个函数开始加锁,函数结束解锁吗?我觉得不必要,细节读者可以自行思考。

漏桶

漏桶又是什么思想呢?简单来说一句话,就是你入水(请求的速度)的速度无所谓多大,我桶漏出水(处理)的速率是固定的,相信大家在网上也看到过很多漏桶的原理图,不再啰嗦,我们直接上代码,以下代码是Uber的开源库。

首先定义一个Limiter的接口


type Limiter interface {
// Take should block to make sure that the RPS is met.
Take() time.Time
}

还有一个clock的接口,先放出来,稍后解释

type Clock interface {
Now() time.Time
Sleep(time.Duration)
}

然后我们给出它的限流器的结构体


type limiter struct {
sync.Mutex
last time.Time //上次请求的时间
sleepFor time.Duration //本次要请求要sleep的时间
perRequest time.Duration //根据限定的速率得出的每次请求分得的时间片
maxSlack time.Duration //稍后解释
clock Clock //时钟(稍后解释)
}

ok,我们来看下创建一个限流器的函数


func New(rate int, opts Option) Limiter {
l := &limiter{
perRequest: time.Second / time.Duration(rate),
maxSlack: -10 time.Second / time.Duration(rate),
}
for _, opt := range opts {
opt(l)
}
if l.clock == nil {
l.clock = clock.New()
}
return l
}

分析下过程:

1.首先我们的perRequest等于1秒钟所包含的纳秒数/我们设定的速率rate,这个好理解

2.maxSlack为-10乘以perRequest,这是为什么呢?我们稍后说。

3.接着,函数传进来了一个Option类型的数组,貌似Option还是函数类型,我们循环调用这些函数。


type Option func(l limiter)

嗯,这些函数应该是给限流器进行配置的。

4.然后创建了一个clock对象,我们稍后看下这个clock的用处是什么。

接下来,我们看看它的Take函数


func (t *limiter) Take() time.Time {
t.Lock()
defer t.Unlock()
<span class="nx">now</span> <span class="o">:=</span> <span class="nx">t</span><span class="p">.</span><span class="nx">clock</span><span class="p">.</span><span class="nf">Now</span><span class="p">()</span>

<span class="k">if</span> <span class="nx">t</span><span class="p">.</span><span class="nx">last</span><span class="p">.</span><span class="nf">IsZero</span><span class="p">()</span> <span class="p">{</span>
	<span class="nx">t</span><span class="p">.</span><span class="nx">last</span> <span class="p">=</span> <span class="nx">now</span>
	<span class="k">return</span> <span class="nx">t</span><span class="p">.</span><span class="nx">last</span>
<span class="p">}</span>

<span class="nx">t</span><span class="p">.</span><span class="nx">sleepFor</span> <span class="o">+=</span> <span class="nx">t</span><span class="p">.</span><span class="nx">perRequest</span> <span class="o">-</span> <span class="nx">now</span><span class="p">.</span><span class="nf">Sub</span><span class="p">(</span><span class="nx">t</span><span class="p">.</span><span class="nx">last</span><span class="p">)</span>

<span class="k">if</span> <span class="nx">t</span><span class="p">.</span><span class="nx">sleepFor</span> <span class="p">&lt;</span> <span class="nx">t</span><span class="p">.</span><span class="nx">maxSlack</span> <span class="p">{</span>
	<span class="nx">t</span><span class="p">.</span><span class="nx">sleepFor</span> <span class="p">=</span> <span class="nx">t</span><span class="p">.</span><span class="nx">maxSlack</span>
<span class="p">}</span>

<span class="k">if</span> <span class="nx">t</span><span class="p">.</span><span class="nx">sleepFor</span> <span class="p">&gt;</span> <span class="mi">0</span> <span class="p">{</span>
	<span class="nx">t</span><span class="p">.</span><span class="nx">clock</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="nx">t</span><span class="p">.</span><span class="nx">sleepFor</span><span class="p">)</span>
	<span class="nx">t</span><span class="p">.</span><span class="nx">last</span> <span class="p">=</span> <span class="nx">now</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="nx">t</span><span class="p">.</span><span class="nx">sleepFor</span><span class="p">)</span>
	<span class="nx">t</span><span class="p">.</span><span class="nx">sleepFor</span> <span class="p">=</span> <span class="mi">0</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
	<span class="nx">t</span><span class="p">.</span><span class="nx">last</span> <span class="p">=</span> <span class="nx">now</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">t</span><span class="p">.</span><span class="nx">last</span>

}

分析下:

1.它加锁了,看起来符合我们的预期

2.首先读取t.clock.Now,ok我们还是来看看这个clock到底是什么玩意


type clock struct{}

func New() Clock {
return &clock{}
}

func (c *clock) After(d time.Duration) <-chan time.Time { return time.After(d) }

func (c *clock) AfterFunc(d time.Duration, f func()) {
// TODO maybe return timer interface
time.AfterFunc(d, f)
}

func (c *clock) Now() time.Time { return time.Now() }

func (c clock) Sleep(d time.Duration) { time.Sleep(d) }

t.clock.Now不就是time.Now()嘛,当前时间点,很好奇干嘛要包一层

3.接下来有一个if判断,t.last.IsZero() 如果为true的话,则把更新t.last为now,然后直接返回t.last,我们先看下IsZero函数

func (t Time) IsZero() bool {
return t.sec 0 && t.nsec 0
}

sec和nsec,这个time的时间点在公元第1年的00:00:00啊,注意看上面初始化我们并没有初始化last,last是time.Time类型,默认初始化为0嘛,可不就IsZero()为true,为了不放心,我写了个小测试代码验证了下,当使用默认初始化time.Time的时候,IsZero()确实为true。

同时,这也符合Uber官方给这段代码的解释,说当第一次请求的时候直接返回。

4.接着更新了一个sleepFor,看起来好像是我们要睡多长时间的意思,看下它的计算方式

t.perRequest - now.Sub(t.last)

perRequest是每次请求需要的时间片,now.Sub(t.last)是本次请求距离上次请求的时间差,喔,到这里我们算是明白Uber是怎么实现漏桶的了,我知道每次请求需要的时间,我根据你上次请求距离本次的时间差,如果到了时间,我就返回,如果没到,我就让你补上时间,怎么补,我让你睡一会。

注意:更新t.sleepFor的时候,不是直接=,而是+=,为什么?因为之前t.sleepFor本身可能为负数,比如上次的请求等了很长时间了,使得当时的sleepFor为很大的负数,本次+=之后还为负数,我们继续直接返回嘛,这样符合我们流出的速度要固定的道理。

5.接下来有一个判断

if t.sleepFor < t.maxSlack {
t.sleepFor = t.maxSlack
}

注意,maxSlack用到了吧,sleepFor比maxSlack还小的时候,我们就让sleepFor赋值为maxSlack,我们回头看下maxSlack等于什么?


maxSlack:   -10  time.Second / time.Duration(rate),

又明白了, 它的意思是sleepFor不能太负,你只能负到-10一次请求的时间片,为什么呢?你想想,如果我某一次请求距离上次请求隔得时间特别长,此时


t.perRequest - now.Sub(t.last)

会负的特别大,那么后面如果突然有激增的流量过来,t.sleepFor +=之后还是负数,直接放过了流量,根本达不到限流的作用,所以,我们把它控制在了-10倍,这个-10应该可以调节,目的是使得我们总的水的流速达到我们的要求,又不能有激增流量过来扛不住。

6.接下来就好理解了,sleepFor为正代表我们要睡会,该睡多长时间睡多长时间。

然后更新last,睡完了清理下sleepFor为0,如果小于0就代表不需要睡,更新下last

7.看起来clock就是time.Time包了一层而已。

7.最后返回当前的时间。

最后,Uber给出了一个测试的用例


func TestExampleRatelimit(t testing.T) {
rl := New(100) // per second
prev := time.Now()
for i := 0; i < 10; i++ {
now := rl.Take()
if i > 0 {
fmt.Println(i, now.Sub(prev))
}
prev = now
}
// Output:
// 1 10ms
// 2 10ms
// 3 10ms
// 4 10ms
// 5 10ms
// 6 10ms
// 7 10ms
// 8 10ms
// 9 10ms
}

很简单,创建一个100QPS的限流器,然后每次调用Take,发现每次请求的间隔都是10ms。


令牌桶

漏桶有一个不好的地方,就是不能处理那种激增的流量,因为流水的速度是固定的,即使资源够用的情况下,处理速度也不会变。于是就有了令牌桶,令牌桶其实跟我们第一种简单限流的思路一样,每次请求的时候都会往里面放令牌,然后再从桶里取令牌,桶有一定的容量限制,当桶里没有令牌的时候则产生限流。看代码


// TokenBucket 令牌桶
type TokenBucket struct {
lastModifiedTime int64 // 上次修改时间
storedTokens uint64 // 桶中存储的令牌数
count uint64 // 每inter时间内产生count个令牌
inter int64 // 产生count个令牌的时间
maxTokens uint64 // 最大的令牌数
sync.RWMutex
}

我们来看下如何创建一个限流器的


func New(count uint64, inter time.Duration, maxTokens uint64, tokensNow uint64, startTime time.Time) TokenBucket {
return &TokenBucket{
count: count,
inter: inter.Nanoseconds(),
maxTokens: maxTokens,
storedTokens: tokensNow,
lastModifiedTime: startTime.UnixNano(),
}
}

解释下:

每隔inter时间产生count个令牌,桶内最多存储maxTokens个令牌,初始状态从startTime开始,桶内有tokensNow个令牌。

我们来看下源码中给出的一个测试用例,我们随着测试用例看


func TestTokenBucketReserve(t testing.T) {
startTime := time.Now()
<span class="nx">bucket</span> <span class="o">:=</span> <span class="nf">New</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">startTime</span><span class="p">)</span>

<span class="k">if</span> <span class="nx">bucket</span><span class="p">.</span><span class="nf">ReserveWithTime</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nx">startTime</span><span class="p">)</span> <span class="p">{</span>
	<span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">"there should be 0 token at %s"</span><span class="p">,</span> <span class="nx">startTime</span><span class="p">.</span><span class="nf">String</span><span class="p">())</span>
<span class="p">}</span>

<span class="c1">// 1秒后可以获取3个

t1 := startTime.Add(time.Second)
if !bucket.ReserveWithTime(3, t1) {
t.Fatalf(“there should be 3 tokens after 1 second”)
}
// 此时已经没有了
if bucket.ReserveWithTime(1, t1) {
t.Fatal(“there should be 0 tokens after reserving 3 tokens”)
}

<span class="c1">// 2秒后有5个token

t1 = t1.Add(2 time.Second)
if !bucket.ReserveWithTime(5, t1) {
t.Fatalf(“there should be 5 tokens after 2 seconds”)
}
// 此时已经没有了
if bucket.ReserveWithTime(1, t1) {
t.Fatal(“there should be 0 tokens after reserving all tokens”)
}
}

分析下:

1.首先创建一个令牌桶,每秒产生3个,最多可存5个,在startTime时桶内只有0个token,startTime是当前时间。

2.接下来调用ReserveWithTime获取令牌,我们来看下这个函数


func (b TokenBucket) ReserveWithTime(count uint64, now time.Time) bool {
if count <= 0 {
return true
}
<span class="nx">b</span><span class="p">.</span><span class="nf">Lock</span><span class="p">()</span>

<span class="nx">b</span><span class="p">.</span><span class="nf">sync</span><span class="p">(</span><span class="nx">now</span><span class="p">.</span><span class="nf">UnixNano</span><span class="p">())</span>

<span class="nx">storedTokens</span> <span class="o">:=</span> <span class="nx">b</span><span class="p">.</span><span class="nx">storedTokens</span>
<span class="k">if</span> <span class="nx">storedTokens</span> <span class="p">&lt;</span> <span class="nx">count</span> <span class="p">{</span>
	<span class="nx">b</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span>
	<span class="k">return</span> <span class="kc">false</span>
<span class="p">}</span>
<span class="nx">b</span><span class="p">.</span><span class="nx">storedTokens</span> <span class="o">-=</span> <span class="nx">count</span>
<span class="nx">b</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span>
<span class="k">return</span> <span class="kc">true</span>

}

看下函数的参数,count即为想要获取的令牌数量,now即为当前请求的时间

1).count为0,直接返回true即可,你不想获取令牌,瞎捣乱

2).同样对限流器的操作也是加锁的

3).调用sync函数,把now传进去,我们看下在干嘛


func (b TokenBucket) sync(nowNano int64) {
diff := nowNano - b.lastModifiedTime
if diff < 0 {
return
}
tokensToPut := uint64(diff/b.inter) b.count
if tokensToPut < 1 {
return
}
<span class="k">if</span> <span class="nx">sum</span><span class="p">,</span> <span class="nx">e</span> <span class="o">:=</span> <span class="nx">b</span><span class="p">.</span><span class="nf">checkedAddUint64</span><span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nx">storedTokens</span><span class="p">,</span> <span class="nx">tokensToPut</span><span class="p">);</span> <span class="nx">e</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span>
	<span class="k">if</span> <span class="nx">sum</span> <span class="p">&gt;</span> <span class="nx">b</span><span class="p">.</span><span class="nx">maxTokens</span> <span class="p">{</span>
		<span class="nx">sum</span> <span class="p">=</span> <span class="nx">b</span><span class="p">.</span><span class="nx">maxTokens</span>
	<span class="p">}</span>
	<span class="nx">b</span><span class="p">.</span><span class="nx">storedTokens</span> <span class="p">=</span> <span class="nx">sum</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
	<span class="k">return</span>
<span class="p">}</span>
<span class="nx">b</span><span class="p">.</span><span class="nx">lastModifiedTime</span> <span class="p">=</span> <span class="nx">nowNano</span>
<span class="k">return</span>

}

首先diff即为本次请求距离上次请求的时间,判断了一个小于0,会出现这种情况吗?其实是会的,比如当请求很多的时候,很多请求与由于锁的原因,被序列化在了锁等待队列里,当锁释放时,当时传进去的time必定比当前时间要小,所以这也恰恰表明了,哪个协程先抢着锁了,哪个协程放令牌。接着计算了下需要放入桶中的令牌数,diff/b.inter等于diff有多少个周期,count是一个周期放多少个令牌。如果不够一个令牌则直接返回,注意这里都没有更新lastModifiedTime,因为这次虽然没有够一个令牌,下次可能够呢,要攒着。接下来调用了checkedAddUint64,看下这个函数在干什么


// sum = a + b,如果溢出,err不为nil。
func (b TokenBucket) checkedAddUint64(n1, n2 uint64) (sum uint64, err error) {
sum = n1 + n2
if !(((n1 ^ n2) < 0) || ((n1 ^ sum) >= 0)) {
err = ErrOverflow
}
return
}

函数的作用是更新令牌总数并且检查了下总数是否溢出,如果溢出则返回错误,我认为这里有错误,n1, n2,sum 都是uint64的整数,


if !(((n1 ^ n2) < 0) || ((n1 ^ sum) >= 0))

永远是false,我认为应该是写成


if sum > n2

比较好,如果溢出了肯定是会小于n2的。这是我个人,如果你有不同意见,可以一起讨论下。

ok,我们继续上面的代码,更新完总的令牌数后,要检测下是否超过最大令牌数了,如果超过了则改为最大令牌数,这个好理解。最后把lastModifiedTime更新为当前时间。

5).sync所谓同步的意思就是更新了lastModifiedTime,并且放入了一定数量的令牌。接下来就简单了,读取 b.storedTokens,获取当前桶内的令牌数量,然后判断我们要获取的桶的数量count是否小于桶内总数量,不够的话返回false,够的话,减去count,返回true。

基本上令牌桶的主要代码我们看完了,我们看下其他几个辅助的函数


func (b TokenBucket) SetRate(count uint64, inter time.Duration) {
b.Lock()
// 先将状态更新到当前时间,然后在设置速度
b.sync(time.Now().UnixNano())
b.count = count
b.inter = inter.Nanoseconds()
b.Unlock()
}

函数很简单,更新速率,速率相关的变量就是count和inter,并且注意把更改时间更新到了当前时间。


func (b TokenBucket) SetMaxTokens(max uint64) {
b.Lock()
// 先将状态更新到当前时间,然后在设置速度
b.sync(time.Now().UnixNano())
b.maxTokens = max
b.Unlock()
return
}

这个函数就是设置桶的最大容量


func (b TokenBucket) GetStoredTokensNow() (tokens uint64) {
b.Lock()
b.sync(time.Now().UnixNano())
tokens = b.storedTokens
b.Unlock()
return
}

获取当前桶的数量。

总结下,令牌桶相比于漏桶有一个好处就是,当桶内有足够的令牌时,突然激增了流量,我们可以很快的把桶内的令牌用完去处理请求,而漏桶则不是,不管你流量多高,我就限定了处理的速度。


一点小想法

可不可以优化下漏桶,桶的思想其实就是生活中的写照,对于漏桶,我想到的就是在桶的底部挖一个小洞,往外流水。但是实际上,洞往外流水的速度是跟桶中水的高度相关的(物理上的压强嘛),如果我们优化下漏桶,往外流水的速度跟桶中水的高度成正比,水越多,我相应的流快一些。当然这只是一个很不成熟的思想,还没有想到具体的实现细节,以及用到真实的工程中是否有效,记录下来以后细想。


后记

技术的成长总是点点滴滴的,希望我能一直保持对技术的热爱。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值