golang服务器开发频率限制 golang.org/x/time/rate 使用说明

官方链接

接口介绍

type Limiter

type Limiter struct {
    // contains filtered or unexported fields
}

Limter限制时间的发生频率,采用令牌池的算法实现。这个池子一开始容量为b,装满b个令牌,然后每秒往里面填充r个令牌。
由于令牌池中最多有b个令牌,所以一次最多只能允许b个事件发生,一个事件花费掉一个令牌。

Limter提供三中主要的函数 Allow, Reserve, and Wait. 大部分时候使用Wait。

func NewLimiter

func NewLimiter(r Limit, b int) *Limiter

NewLimiter 返回一个新的Limiter。

func (*Limiter) [Allow]

func (lim *Limiter) Allow() bool

Allow 是函数 AllowN(time.Now(), 1)的简化函数。

func (*Limiter) AllowN

func (lim *Limiter) AllowN(now time.Time, n int) bool

AllowN标识在时间now的时候,n个事件是否可以同时发生(也意思就是now的时候是否可以从令牌池中取n个令牌)。如果你需要在事件超出频率的时候丢弃或跳过事件,就使用AllowN,否则使用Reserve或Wait.

func (*Limiter) Reserve

func (lim *Limiter) Reserve() *Reservation

Reserve是ReserveN(time.Now(), 1).的简化形式。

func (*Limiter) ReserveN

func (lim *Limiter) ReserveN(now time.Time, n int) *Reservation

ReserveN 返回对象Reservation ,标识调用者需要等多久才能等到n个事件发生(意思就是等多久令牌池中至少含有n个令牌)。

如果ReserveN 传入的n大于令牌池的容量b,那么返回false.
使用样例如下:

r := lim.ReserveN(time.Now(), 1)
if !r.OK() {
  // Not allowed to act! Did you remember to set lim.burst to be > 0 ?我只要1个事件发生仍然返回false,是不是b设置为了0?
  return
}
time.Sleep(r.Delay())
Act()

如果希望根据频率限制等待和降低事件发生的速度而不丢掉事件,就使用这个方法。
我认为这里要表达的意思就是如果事件发生的频率是可以由调用者控制的话,可以用ReserveN 来控制事件发生的速度而不丢掉事件。如果要使用context的截止日期或cancel方法的话,使用WaitN。

func (*Limiter) Wait

func (lim *Limiter) Wait(ctx context.Context) (err error)

Wait是WaitN(ctx, 1)的简化形式。

func (*Limiter) WaitN

func (lim *Limiter) WaitN(ctx context.Context, n int) (err error)

WaitN 阻塞当前直到lim允许n个事件的发生。

  • 如果n超过了令牌池的容量大小则报错。
  • 如果Context被取消了则报错。
  • 如果lim的等待时间超过了Context的超时时间则报错。

样例

测试 AllowN

package main

import (
   "os"
   "time"

   "golang.org/x/time/rate"

   "github.com/op/go-logging"
)

var log = logging.MustGetLogger("example")

// Example format string. Everything except the message has a custom color
// which is dependent on the log level. Many fields have a custom output
// formatting too, eg. the time returns the hour down to the milli second.
var format = logging.MustStringFormatter(
   `%{color}%{time:15:04:05.000} %{shortfunc} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
)

func main() {

   backend1 := logging.NewLogBackend(os.Stderr, "", 0)
   backend2 := logging.NewLogBackend(os.Stderr, "", 0)
   backend2Formatter := logging.NewBackendFormatter(backend2, format)
   backend1Leveled := logging.AddModuleLevel(backend1)
   backend1Leveled.SetLevel(logging.ERROR, "")
   logging.SetBackend(backend1Leveled, backend2Formatter)

   r := rate.Every(1)
   limit := rate.NewLimiter(r, 10)
   for {
       if limit.AllowN(time.Now(), 8) {
           log.Info("log:event happen")
       } else {
           log.Info("log:event not allow")
       }

   }

}

网络请求频率限制大多用的Allow,如tollbooth,服务器在对每一个请求相应之前,先从令牌池中获取令牌,如果没有令牌可用,则忽略或丢弃当前请求。
tollbooth 可以基于方法,IP等进行限制,基本实现方法就是把方法、ip作为一个key,然后对每一个key关联一个Limiter。

    // Map of limiters without TTL
    tokenBucketsNoTTL map[string]*rate.Limiter

    // Map of limiters with TTL
    tokenBucketsWithTTL *gocache.Cache

gocache.Cache 也是一个map,不过还实现了”有效期“功能,如果某个key超过了有效期就会从map中清除,这个机制实现的很巧妙,会在以后的文章中介绍。

然后对于每一个网络请求,提取出key,然后判断key对应的Limiter是否有可用的令牌,如下:

func (l *Limiter) limitReachedNoTokenBucketTTL(key string) bool {
    l.Lock()
    defer l.Unlock()

    if _, found := l.tokenBucketsNoTTL[key]; !found {
        l.tokenBucketsNoTTL[key] = rate.NewLimiter(rate.Every(l.TTL), int(l.Max))
    }

    return !l.tokenBucketsNoTTL[key].AllowN(time.Now(), 1)
}

...

func (l *Limiter) limitReachedWithCustomTokenBucketTTL(key string, tokenBucketTTL time.Duration) bool {
    l.Lock()
    defer l.Unlock()

    if _, found := l.tokenBucketsWithTTL.Get(key); !found {
        l.tokenBucketsWithTTL.Set(
            key,
            rate.NewLimiter(rate.Every(l.TTL), int(l.Max)),
            tokenBucketTTL,
        )
    }

    expiringMap, found := l.tokenBucketsWithTTL.Get(key)
    if !found {
        return false
    }

    return !expiringMap.(*rate.Limiter).AllowN(time.Now(), 1)
}

测试ReserveN

参考YY哥

package main

import (
    "bytes"
    "fmt"
    "io"
    "time"

    "golang.org/x/time/rate"
)

type reader struct {
    r      io.Reader
    limiter *rate.Limiter
}

// Reader returns a reader that is rate limited by
// the given token bucket. Each token in the bucket
// represents one byte.
func NewReader(r io.Reader, l *rate.Limiter) io.Reader {
    return &reader{
        r:      r,
        limiter:l,
    }
}

func (r *reader) Read(buf []byte) (int, error) {
    n, err := r.r.Read(buf)
    if n <= 0 {
        return n, err
    }

    now := time.Now()
    rv := r.limiter.ReserveN(now, n)
    if !rv.OK() {
        return 0, fmt.Errorf("Exceeds limiter's burst")
    }
    delay := rv.DelayFrom(now)
    //fmt.Printf("Read %d bytes, delay %d\n", n, delay)
    time.Sleep(delay)
    return n, err
}

func main() {
    // Source holding 1MB
    src := bytes.NewReader(make([]byte, 1024*1024))
    // Destination
    dst := &bytes.Buffer{}

    // Bucket adding 100KB every second, holding max 100KB
    limit := rate.NewLimiter(100*1024, 100*1024)

    start := time.Now()

    buf := make([]byte, 10*1024)
    // Copy source to destination, but wrap our reader with rate limited one
    //io.CopyBuffer(dst, NewReader(src, limit), buf)
    r := NewReader(src, limit)
    for{
        if n, err := r.Read(buf); err == nil {
            dst.Write(buf[0:n])
        }else{
            break
        }
    }

    fmt.Printf("Copied %d bytes in %s\n", dst.Len(), time.Since(start))
}

后记

之前一直在CSDN上写文章,后面会逐步转换到简书上,还请大家多多支持。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,009评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,808评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,891评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,283评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,285评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,409评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,809评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,487评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,680评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,499评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,548评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,268评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,815评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,872评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,102评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,683评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,253评论 2 341

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,573评论 18 139
  • 1 来到公司已经一个月了,但是感觉已经过了一年的时间。 前两天晨会,公司董事长老夏宣布一个消息:我们公司过两天装几...
    邮递员王先森阅读 351评论 4 2
  • 午后,阳光淡然,斟一杯咖啡,空气中流动着属于秋日独有的懒洋洋的气氛,安静的看完这部《咕咕是一只猫》,心情便在电影缓...
    暖M暖阅读 254评论 0 1