橄榄树林 發表於 2020-9-14 23:30:00

漏桶、令牌桶限流的Go语言实现

<p style="text-align: center"><span style="font-size: 18px"><strong>本文首发于我的个人博客:liwenzhou.com,更多更详细的Go语言项目实战内容就在liwenzhou.com。</strong></span></p>
<h2 style="margin: 1em auto; padding: 0 0 0 10px; font-weight: bold; font-size: 22px; color: rgba(0, 150, 136, 1); border-left: 3px solid rgba(0, 150, 136, 1)" data-tool="mdnice编辑器"><span class="content">限流</span></h2>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">限流又称为流量控制(流控),通常是指限制到达系统的并发请求数。</p>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">我们生活中也会经常遇到限流的场景,比如:某景区限制每日进入景区的游客数量为8万人;沙河地铁站早高峰通过站外排队逐一放行的方式限制同一时间进入车站的旅客数量等。</p>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">限流虽然会影响部分用户的使用体验,但是却能在一定程度上报障系统的稳定性,不至于崩溃(大家都没了用户体验)。</p>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">而互联网上类似需要限流的业务场景也有很多,比如电商系统的秒杀、微博上突发热点新闻、双十一购物节、12306抢票等等。这些场景下的用户请求量通常会激增,远远超过平时正常的请求量,此时如果不加任何限制很容易就会将后端服务打垮,影响服务的稳定性。</p>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">此外,一些厂商公开的API服务通常也会限制用户的请求次数,比如百度地图开放平台等会根据用户的付费情况来限制用户的请求数等。 <img src="https://liwenzhou.com/images/Go/ratelimit/baidumap_ratelimit.jpg" alt="百度地图开放平台API调用策略" style="display: block; margin: 0 auto; max-width: 100%"></p>
<h2 style="margin: 1em auto; padding: 0 0 0 10px; font-weight: bold; font-size: 22px; color: rgba(0, 150, 136, 1); border-left: 3px solid rgba(0, 150, 136, 1)" data-tool="mdnice编辑器"><span class="content">常用的限流策略</span></h2>
<h3 style="margin: 0.6em auto; padding: 0 0 0 10px; font-weight: bold; color: rgba(0, 0, 0, 1); font-size: 20px; border-left: 2px solid rgba(0, 150, 136, 1)" data-tool="mdnice编辑器"><span class="content">漏桶</span></h3>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">漏桶法限流很好理解,假设我们有一个水桶按固定的速率向下方滴落一滴水,无论有多少请求,请求的速率有多大,都按照固定的速率流出,对应到系统中就是按照固定的速率处理请求。</p>
<p><img src="https://liwenzhou.com/images/Go/ratelimit/loutong.jpg" alt="漏桶算法原理" style="display: block; margin: 0 auto; max-width: 100%">漏桶算法原理</p>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">漏桶法的关键点在于漏桶始终按照固定的速率运行,但是它并不能很好的处理有大量突发请求的场景,毕竟在某些场景下我们可能需要提高系统的处理效率,而不是一味的按照固定速率处理请求。</p>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">关于漏桶的实现,uber团队有一个开源的github.com/uber-go/ratelimit库。 这个库的使用方法比较简单,<code style="font-size: 14px; word-wrap: break-word; padding: 2px 4px; border-radius: 4px; margin: 0 2px; background-color: rgba(27, 31, 35, 0.05); font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; word-break: break-all; color: rgba(0, 150, 136, 1)">Take()</code> 方法会返回漏桶下一次滴水的时间。</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="overflow-x: auto; padding: 16px; color: rgba(51, 51, 51, 1); background: rgba(248, 248, 248, 1); display: block; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; border-radius: 0; font-size: 12px; -webkit-overflow-scrolling: touch"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">import</span> (
<span class="hljs-string" style="color: rgba(221, 17, 68, 1); line-height: 26px">"fmt"</span>
<span class="hljs-string" style="color: rgba(221, 17, 68, 1); line-height: 26px">"time"</span>

<span class="hljs-string" style="color: rgba(221, 17, 68, 1); line-height: 26px">"go.uber.org/ratelimit"</span>
)

<span class="hljs-function" style="line-height: 26px"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">func</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">main</span><span class="hljs-params" style="line-height: 26px">()</span></span> {
    rl := ratelimit.New(<span class="hljs-number" style="color: rgba(0, 128, 128, 1); line-height: 26px">100</span>) <span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// per second</span>

    prev := time.Now()
    <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">for</span> i := <span class="hljs-number" style="color: rgba(0, 128, 128, 1); line-height: 26px">0</span>; i &lt; <span class="hljs-number" style="color: rgba(0, 128, 128, 1); line-height: 26px">10</span>; i++ {
      now := rl.Take()
      fmt.Println(i, now.Sub(prev))
      prev = now
    }

    <span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// Output:</span>
    <span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 0 0</span>
    <span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 1 10ms</span>
    <span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 2 10ms</span>
    <span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 3 10ms</span>
    <span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 4 10ms</span>
    <span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 5 10ms</span>
    <span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 6 10ms</span>
    <span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 7 10ms</span>
    <span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 8 10ms</span>
    <span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 9 10ms</span>
}
</code></pre>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">它的源码实现也比较简单,这里大致说一下关键的地方,有兴趣的同学可以自己去看一下完整的源码。</p>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">限制器是一个接口类型,其要求实现一个<code style="font-size: 14px; word-wrap: break-word; padding: 2px 4px; border-radius: 4px; margin: 0 2px; background-color: rgba(27, 31, 35, 0.05); font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; word-break: break-all; color: rgba(0, 150, 136, 1)">Take()</code>方法:</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="overflow-x: auto; padding: 16px; color: rgba(51, 51, 51, 1); background: rgba(248, 248, 248, 1); display: block; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; border-radius: 0; font-size: 12px; -webkit-overflow-scrolling: touch"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">type</span> Limiter <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">interface</span> {
<span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// Take方法应该阻塞已确保满足 RPS</span>
Take() time.Time
}
</code></pre>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">实现限制器接口的结构体定义如下,这里可以重点留意下<code style="font-size: 14px; word-wrap: break-word; padding: 2px 4px; border-radius: 4px; margin: 0 2px; background-color: rgba(27, 31, 35, 0.05); font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; word-break: break-all; color: rgba(0, 150, 136, 1)">maxSlack</code>字段,它在后面的<code style="font-size: 14px; word-wrap: break-word; padding: 2px 4px; border-radius: 4px; margin: 0 2px; background-color: rgba(27, 31, 35, 0.05); font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; word-break: break-all; color: rgba(0, 150, 136, 1)">Take()</code>方法中的处理。</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="overflow-x: auto; padding: 16px; color: rgba(51, 51, 51, 1); background: rgba(248, 248, 248, 1); display: block; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; border-radius: 0; font-size: 12px; -webkit-overflow-scrolling: touch"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">type</span> limiter <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">struct</span> {
sync.Mutex                <span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 锁</span>
last       time.Time      <span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 上一次的时刻</span>
sleepFor   time.Duration<span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 需要等待的时间</span>
perRequest time.Duration<span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 每次的时间间隔</span>
maxSlack   time.Duration<span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 最大的富余量</span>
clock      Clock          <span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 时钟</span>
}
</code></pre>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器"><code style="font-size: 14px; word-wrap: break-word; padding: 2px 4px; border-radius: 4px; margin: 0 2px; background-color: rgba(27, 31, 35, 0.05); font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; word-break: break-all; color: rgba(0, 150, 136, 1)">limiter</code>结构体实现<code style="font-size: 14px; word-wrap: break-word; padding: 2px 4px; border-radius: 4px; margin: 0 2px; background-color: rgba(27, 31, 35, 0.05); font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; word-break: break-all; color: rgba(0, 150, 136, 1)">Limiter</code>接口的<code style="font-size: 14px; word-wrap: break-word; padding: 2px 4px; border-radius: 4px; margin: 0 2px; background-color: rgba(27, 31, 35, 0.05); font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; word-break: break-all; color: rgba(0, 150, 136, 1)">Take()</code>方法内容如下:</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="overflow-x: auto; padding: 16px; color: rgba(51, 51, 51, 1); background: rgba(248, 248, 248, 1); display: block; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; border-radius: 0; font-size: 12px; -webkit-overflow-scrolling: touch"><span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// Take 会阻塞确保两次请求之间的时间走完</span>
<span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// Take 调用平均数为 time.Second/rate.</span>
<span class="hljs-function" style="line-height: 26px"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">func</span> <span class="hljs-params" style="line-height: 26px">(t *limiter)</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">Take</span><span class="hljs-params" style="line-height: 26px">()</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">time</span>.<span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">Time</span></span> {
t.Lock()
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">defer</span> t.Unlock()

now := t.clock.Now()

<span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 如果是第一次请求就直接放行</span>
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">if</span> t.last.IsZero() {
t.last = now
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">return</span> t.last
}

<span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// sleepFor 根据 perRequest 和上一次请求的时刻计算应该sleep的时间</span>
<span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 由于每次请求间隔的时间可能会超过perRequest, 所以这个数字可能为负数,并在多个请求之间累加</span>
t.sleepFor += t.perRequest - now.Sub(t.last)

<span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 我们不应该让sleepFor负的太多,因为这意味着一个服务在短时间内慢了很多随后会得到更高的RPS。</span>
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">if</span> t.sleepFor &lt; t.maxSlack {
t.sleepFor = t.maxSlack
}

<span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 如果 sleepFor 是正值那么就 sleep</span>
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">if</span> t.sleepFor &gt; <span class="hljs-number" style="color: rgba(0, 128, 128, 1); line-height: 26px">0</span> {
t.clock.Sleep(t.sleepFor)
t.last = now.Add(t.sleepFor)
t.sleepFor = <span class="hljs-number" style="color: rgba(0, 128, 128, 1); line-height: 26px">0</span>
} <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">else</span> {
t.last = now
}

<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">return</span> t.last
}
</code></pre>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">上面的代码根据记录每次请求的间隔时间和上一次请求的时刻来计算当次请求需要阻塞的时间——<code style="font-size: 14px; word-wrap: break-word; padding: 2px 4px; border-radius: 4px; margin: 0 2px; background-color: rgba(27, 31, 35, 0.05); font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; word-break: break-all; color: rgba(0, 150, 136, 1)">sleepFor</code>,这里需要留意的是<code style="font-size: 14px; word-wrap: break-word; padding: 2px 4px; border-radius: 4px; margin: 0 2px; background-color: rgba(27, 31, 35, 0.05); font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; word-break: break-all; color: rgba(0, 150, 136, 1)">sleepFor</code>的值可能为负,在经过间隔时间长的两次访问之后会导致随后大量的请求被放行,所以代码中针对这个场景有专门的优化处理。创建限制器的<code style="font-size: 14px; word-wrap: break-word; padding: 2px 4px; border-radius: 4px; margin: 0 2px; background-color: rgba(27, 31, 35, 0.05); font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; word-break: break-all; color: rgba(0, 150, 136, 1)">New()</code>函数中会为<code style="font-size: 14px; word-wrap: break-word; padding: 2px 4px; border-radius: 4px; margin: 0 2px; background-color: rgba(27, 31, 35, 0.05); font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; word-break: break-all; color: rgba(0, 150, 136, 1)">maxSlack</code>设置初始值,也可以通过<code style="font-size: 14px; word-wrap: break-word; padding: 2px 4px; border-radius: 4px; margin: 0 2px; background-color: rgba(27, 31, 35, 0.05); font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; word-break: break-all; color: rgba(0, 150, 136, 1)">WithoutSlack</code>这个Option取消这个默认值。</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="overflow-x: auto; padding: 16px; color: rgba(51, 51, 51, 1); background: rgba(248, 248, 248, 1); display: block; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; border-radius: 0; font-size: 12px; -webkit-overflow-scrolling: touch"><span class="hljs-function" style="line-height: 26px"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">func</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">New</span><span class="hljs-params" style="line-height: 26px">(rate <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">int</span>, opts ...Option)</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">Limiter</span></span> {
l := &amp;limiter{
perRequest: time.Second / time.Duration(rate),
maxSlack:   <span class="hljs-number" style="color: rgba(0, 128, 128, 1); line-height: 26px">-10</span> * time.Second / time.Duration(rate),
}
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">for</span> _, opt := <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">range</span> opts {
opt(l)
}
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">if</span> l.clock == <span class="hljs-literal" style="color: rgba(0, 128, 128, 1); line-height: 26px">nil</span> {
l.clock = clock.New()
}
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">return</span> l
}
</code></pre>
<h3 style="margin: 0.6em auto; padding: 0 0 0 10px; font-weight: bold; color: rgba(0, 0, 0, 1); font-size: 20px; border-left: 2px solid rgba(0, 150, 136, 1)" data-tool="mdnice编辑器"><span class="content">令牌桶</span></h3>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">令牌桶其实和漏桶的原理类似,令牌桶按固定的速率往桶里放入令牌,并且只要能从桶里取出令牌就能通过,令牌桶支持突发流量的快速处理。</p>
<p><img src="https://liwenzhou.com/images/Go/ratelimit/lingpaitong.jpg" alt="令牌桶原理" style="display: block; margin: 0 auto; max-width: 100%">令牌桶原理</p>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">对于从桶里取不到令牌的场景,我们可以选择等待也可以直接拒绝并返回。</p>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">对于令牌桶的Go语言实现,大家可以参照github.com/juju/ratelimit库。这个库支持多种令牌桶模式,并且使用起来也比较简单。</p>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">创建令牌桶的方法:</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="overflow-x: auto; padding: 16px; color: rgba(51, 51, 51, 1); background: rgba(248, 248, 248, 1); display: block; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; border-radius: 0; font-size: 12px; -webkit-overflow-scrolling: touch"><span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 创建指定填充速率和容量大小的令牌桶</span>
<span class="hljs-function" style="line-height: 26px"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">func</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">NewBucket</span><span class="hljs-params" style="line-height: 26px">(fillInterval time.Duration, capacity <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">int64</span>)</span> *<span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">Bucket</span></span>
<span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 创建指定填充速率、容量大小和每次填充的令牌数的令牌桶</span>
<span class="hljs-function" style="line-height: 26px"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">func</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">NewBucketWithQuantum</span><span class="hljs-params" style="line-height: 26px">(fillInterval time.Duration, capacity, quantum <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">int64</span>)</span> *<span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">Bucket</span></span>
<span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 创建填充速度为指定速率和容量大小的令牌桶</span>
<span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// NewBucketWithRate(0.1, 200) 表示每秒填充20个令牌</span>
<span class="hljs-function" style="line-height: 26px"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">func</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">NewBucketWithRate</span><span class="hljs-params" style="line-height: 26px">(rate <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">float64</span>, capacity <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">int64</span>)</span> *<span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">Bucket</span></span>
</code></pre>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">取出令牌的方法如下:</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="overflow-x: auto; padding: 16px; color: rgba(51, 51, 51, 1); background: rgba(248, 248, 248, 1); display: block; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; border-radius: 0; font-size: 12px; -webkit-overflow-scrolling: touch"><span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 取token(非阻塞)</span>
<span class="hljs-function" style="line-height: 26px"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">func</span> <span class="hljs-params" style="line-height: 26px">(tb *Bucket)</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">Take</span><span class="hljs-params" style="line-height: 26px">(count <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">int64</span>)</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">time</span>.<span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">Duration</span></span>
<span class="hljs-function" style="line-height: 26px"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">func</span> <span class="hljs-params" style="line-height: 26px">(tb *Bucket)</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">TakeAvailable</span><span class="hljs-params" style="line-height: 26px">(count <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">int64</span>)</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">int64</span></span>

<span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 最多等maxWait时间取token</span>
<span class="hljs-function" style="line-height: 26px"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">func</span> <span class="hljs-params" style="line-height: 26px">(tb *Bucket)</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">TakeMaxDuration</span><span class="hljs-params" style="line-height: 26px">(count <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">int64</span>, maxWait time.Duration)</span> <span class="hljs-params" style="line-height: 26px">(time.Duration, <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">bool</span>)</span></span>

<span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 取token(阻塞)</span>
<span class="hljs-function" style="line-height: 26px"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">func</span> <span class="hljs-params" style="line-height: 26px">(tb *Bucket)</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">Wait</span><span class="hljs-params" style="line-height: 26px">(count <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">int64</span>)</span></span>
<span class="hljs-function" style="line-height: 26px"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">func</span> <span class="hljs-params" style="line-height: 26px">(tb *Bucket)</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">WaitMaxDuration</span><span class="hljs-params" style="line-height: 26px">(count <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">int64</span>, maxWait time.Duration)</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">bool</span></span>
</code></pre>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">虽说是令牌桶,但是我们没有必要真的去生成令牌放到桶里,我们只需要每次来取令牌的时候计算一下,当前是否有足够的令牌就可以了,具体的计算方式可以总结为下面的公式:</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="overflow-x: auto; padding: 16px; color: rgba(51, 51, 51, 1); background: rgba(248, 248, 248, 1); display: block; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; border-radius: 0; font-size: 12px; -webkit-overflow-scrolling: touch">当前令牌数 = 上一次剩余的令牌数 + (本次取令牌的时刻-上一次取令牌的时刻)/放置令牌的时间间隔 * 每次放置的令牌数
</code></pre>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">github.com/juju/ratelimit这个库中关于令牌数计算的源代码如下:</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="overflow-x: auto; padding: 16px; color: rgba(51, 51, 51, 1); background: rgba(248, 248, 248, 1); display: block; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; border-radius: 0; font-size: 12px; -webkit-overflow-scrolling: touch"><span class="hljs-function" style="line-height: 26px"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">func</span> <span class="hljs-params" style="line-height: 26px">(tb *Bucket)</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">currentTick</span><span class="hljs-params" style="line-height: 26px">(now time.Time)</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">int64</span></span> {
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">return</span> <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">int64</span>(now.Sub(tb.startTime) / tb.fillInterval)
}
</code></pre>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="overflow-x: auto; padding: 16px; color: rgba(51, 51, 51, 1); background: rgba(248, 248, 248, 1); display: block; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; border-radius: 0; font-size: 12px; -webkit-overflow-scrolling: touch"><span class="hljs-function" style="line-height: 26px"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">func</span> <span class="hljs-params" style="line-height: 26px">(tb *Bucket)</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">adjustavailableTokens</span><span class="hljs-params" style="line-height: 26px">(tick <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">int64</span>)</span></span> {
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">if</span> tb.availableTokens &gt;= tb.capacity {
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">return</span>
}
tb.availableTokens += (tick - tb.latestTick) * tb.quantum
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">if</span> tb.availableTokens &gt; tb.capacity {
tb.availableTokens = tb.capacity
}
tb.latestTick = tick
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">return</span>
}
</code></pre>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">获取令牌的<code style="font-size: 14px; word-wrap: break-word; padding: 2px 4px; border-radius: 4px; margin: 0 2px; background-color: rgba(27, 31, 35, 0.05); font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; word-break: break-all; color: rgba(0, 150, 136, 1)">TakeAvailable()</code>函数关键部分的源代码如下:</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="overflow-x: auto; padding: 16px; color: rgba(51, 51, 51, 1); background: rgba(248, 248, 248, 1); display: block; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; border-radius: 0; font-size: 12px; -webkit-overflow-scrolling: touch"><span class="hljs-function" style="line-height: 26px"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">func</span> <span class="hljs-params" style="line-height: 26px">(tb *Bucket)</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">takeAvailable</span><span class="hljs-params" style="line-height: 26px">(now time.Time, count <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">int64</span>)</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">int64</span></span> {
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">if</span> count &lt;= <span class="hljs-number" style="color: rgba(0, 128, 128, 1); line-height: 26px">0</span> {
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">return</span> <span class="hljs-number" style="color: rgba(0, 128, 128, 1); line-height: 26px">0</span>
}
tb.adjustavailableTokens(tb.currentTick(now))
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">if</span> tb.availableTokens &lt;= <span class="hljs-number" style="color: rgba(0, 128, 128, 1); line-height: 26px">0</span> {
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">return</span> <span class="hljs-number" style="color: rgba(0, 128, 128, 1); line-height: 26px">0</span>
}
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">if</span> count &gt; tb.availableTokens {
count = tb.availableTokens
}
tb.availableTokens -= count
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">return</span> count
}
</code></pre>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">大家从代码中也可以看到其实令牌桶的实现并没有很复杂。</p>
<h2 style="margin: 1em auto; padding: 0 0 0 10px; font-weight: bold; font-size: 22px; color: rgba(0, 150, 136, 1); border-left: 3px solid rgba(0, 150, 136, 1)" data-tool="mdnice编辑器"><span class="content">gin框架中使用限流中间件</span></h2>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">在gin框架构建的项目中,我们可以将限流组件定义成中间件。</p>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">这里使用令牌桶作为限流策略,编写一个限流中间件如下:</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="overflow-x: auto; padding: 16px; color: rgba(51, 51, 51, 1); background: rgba(248, 248, 248, 1); display: block; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; border-radius: 0; font-size: 12px; -webkit-overflow-scrolling: touch"><span class="hljs-function" style="line-height: 26px"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">func</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">RateLimitMiddleware</span><span class="hljs-params" style="line-height: 26px">(fillInterval time.Duration, <span class="hljs-built_in" style="color: rgba(0, 134, 179, 1); line-height: 26px">cap</span> <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">int64</span>)</span> <span class="hljs-title" style="color: rgba(153, 0, 0, 1); font-weight: bold; line-height: 26px">func</span><span class="hljs-params" style="line-height: 26px">(c *gin.Context)</span></span> {
bucket := ratelimit.NewBucket(fillInterval, <span class="hljs-built_in" style="color: rgba(0, 134, 179, 1); line-height: 26px">cap</span>)
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">return</span> <span class="hljs-function" style="line-height: 26px"><span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">func</span><span class="hljs-params" style="line-height: 26px">(c *gin.Context)</span></span> {
<span class="hljs-comment" style="color: rgba(153, 153, 136, 1); font-style: italic; line-height: 26px">// 如果取不到令牌就中断本次请求返回 rate limit...</span>
<span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">if</span> bucket.TakeAvailable(<span class="hljs-number" style="color: rgba(0, 128, 128, 1); line-height: 26px">1</span>) &lt; <span class="hljs-number" style="color: rgba(0, 128, 128, 1); line-height: 26px">1</span> {
   c.String(http.StatusOK, <span class="hljs-string" style="color: rgba(221, 17, 68, 1); line-height: 26px">"rate limit..."</span>)
   c.Abort()
   <span class="hljs-keyword" style="color: rgba(51, 51, 51, 1); font-weight: bold; line-height: 26px">return</span>
}
c.Next()
}
}
</code></pre>
<p style="font-size: 16px; padding-top: 8px; padding-bottom: 8px; margin: 0; line-height: 26px; color: rgba(0, 0, 0, 1); text-align: justify" data-tool="mdnice编辑器">对于该限流中间件的注册位置,我们可以按照不同的限流策略将其注册到不同的位置,例如:</p>
<ol style="margin-top: 8px; margin-bottom: 8px; padding-left: 25px; color: rgba(0, 0, 0, 1); list-style-type: decimal" data-tool="mdnice编辑器">
<li>如果要对全站限流就可以注册成全局的中间件。</li>
<li>如果是某一组路由需要限流,那么就只需将该限流中间件注册到对应的路由组即可。</li>
</ol><hr>
<p>&nbsp;</p>
<p>本文首发于我的个人博客:liwenzhou.com</p>
<div id="gtx-trans" style="position: absolute; left: 987px; top: -17px">&nbsp;</div><br><br>
来源:https://www.cnblogs.com/liwenzhou/p/13670165.html
頁: [1]
查看完整版本: 漏桶、令牌桶限流的Go语言实现