当前位置:首页 > 科技  > 软件

Go-Zero 是如何实现令牌桶限流的?

来源: 责编: 时间:2023-08-14 22:01:53 3758观看
导读上一篇文章介绍了 如何实现计数器限流。主要有两种实现方式,分别是固定窗口和滑动窗口,并且分析了 go-zero 采用固定窗口方式实现的源码。但是采用固定窗口实现的限流器会有两个问题:会出现请求量超出限制值两倍的情况无

上一篇文章介绍了 如何实现计数器限流。主要有两种实现方式,分别是固定窗口和滑动窗口,并且分析了 go-zero 采用固定窗口方式实现的源码。Ljl28资讯网——每日最新资讯28at.com

但是采用固定窗口实现的限流器会有两个问题:Ljl28资讯网——每日最新资讯28at.com

  1. 会出现请求量超出限制值两倍的情况
  2. 无法很好处理流量突增问题

这篇文章来介绍一下令牌桶算法,可以很好解决以上两个问题。Ljl28资讯网——每日最新资讯28at.com

工作原理

算法概念如下:Ljl28资讯网——每日最新资讯28at.com

  • 令牌以固定速率生成;
  • 生成的令牌放入令牌桶中存放,如果令牌桶满了则多余的令牌会直接丢弃,当请求到达时,会尝试从令牌桶中取令牌,取到了令牌的请求可以执行;
  • 如果桶空了,那么尝试取令牌的请求会被直接丢弃。

图片图片Ljl28资讯网——每日最新资讯28at.com

令牌桶算法既能够将所有的请求平均分布到时间区间内,又能接受服务器能够承受范围内的突发请求,因此是目前使用较为广泛的一种限流算法。Ljl28资讯网——每日最新资讯28at.com

源码实现

源码分析我们还是以 go-zero 项目为例,首先来看生成令牌的部分,依然是使用 Redis 来实现。Ljl28资讯网——每日最新资讯28at.com

// core/limit/tokenlimit.go// 生成 token 速率script = `local rate = tonumber(ARGV[1])// 通容量local capacity = tonumber(ARGV[2])// 当前时间戳local now = tonumber(ARGV[3])// 请求数量local requested = tonumber(ARGV[4])// 需要多少秒才能把桶填满local fill_time = capacity/rate// 向下取整,ttl 为填满时间 2 倍local ttl = math.floor(fill_time*2)// 当前桶剩余容量,如果为 nil,说明第一次使用,赋值为桶最大容量local last_tokens = tonumber(redis.call("get", KEYS[1]))if last_tokens == nil then    last_tokens = capacityend// 上次请求时间戳,如果为 nil 则赋值 0local last_refreshed = tonumber(redis.call("get", KEYS[2]))if last_refreshed == nil then    last_refreshed = 0end// 距离上一次请求的时间跨度local delta = math.max(0, now-last_refreshed)// 距离上一次请求的时间跨度能生成的 token 数量和桶内剩余 token 数量的和// 与桶容量比较,取二者的小值local filled_tokens = math.min(capacity, last_tokens+(delta*rate))// 判断请求数量和桶内 token 数量的大小local allowed = filled_tokens >= requested// 被请求消耗掉之后,更新剩余 token 数量local new_tokens = filled_tokensif allowed then    new_tokens = filled_tokens - requestedend// 更新 redis tokenredis.call("setex", KEYS[1], ttl, new_tokens)// 更新 redis 刷新时间redis.call("setex", KEYS[2], ttl, now)return allowed`

Redis 中主要保存两个 key,分别是 token 数量和刷新时间。Ljl28资讯网——每日最新资讯28at.com

核心思想就是比较两次请求时间间隔内生成的 token 数量 + 桶内剩余 token 数量,和请求量之间的大小,如果满足则允许,否则则不允许。Ljl28资讯网——每日最新资讯28at.com

限流器初始化:Ljl28资讯网——每日最新资讯28at.com

// A TokenLimiter controls how frequently events are allowed to happen with in one second.type TokenLimiter struct {    // 生成 token 速率    rate           int    // 桶容量    burst          int    store          *redis.Redis    // 桶 key    tokenKey       string    // 桶刷新时间 key    timestampKey   string    rescueLock     sync.Mutex    // redis 健康标识    redisAlive     uint32    // redis 健康监控启动状态    monitorStarted bool    // 内置单机限流器    rescueLimiter  *xrate.Limiter}// NewTokenLimiter returns a new TokenLimiter that allows events up to rate and permits// bursts of at most burst tokens.func NewTokenLimiter(rate, burst int, store *redis.Redis, key string) *TokenLimiter {    tokenKey := fmt.Sprintf(tokenFormat, key)    timestampKey := fmt.Sprintf(timestampFormat, key)    return &TokenLimiter{        rate:          rate,        burst:         burst,        store:         store,        tokenKey:      tokenKey,        timestampKey:  timestampKey,        redisAlive:    1,        rescueLimiter: xrate.NewLimiter(xrate.Every(time.Second/time.Duration(rate)), burst),    }}

其中有一个变量 rescueLimiter,这是一个进程内的限流器。如果 Redis 发生故障了,那么就使用这个,算是一个保障,尽量避免系统被突发流量拖垮。Ljl28资讯网——每日最新资讯28at.com

图片图片Ljl28资讯网——每日最新资讯28at.com

提供了四个可调用方法:Ljl28资讯网——每日最新资讯28at.com

// Allow is shorthand for AllowN(time.Now(), 1).func (lim *TokenLimiter) Allow() bool {    return lim.AllowN(time.Now(), 1)}// AllowCtx is shorthand for AllowNCtx(ctx,time.Now(), 1) with incoming context.func (lim *TokenLimiter) AllowCtx(ctx context.Context) bool {    return lim.AllowNCtx(ctx, time.Now(), 1)}// AllowN reports whether n events may happen at time now.// Use this method if you intend to drop / skip events that exceed the rate.// Otherwise, use Reserve or Wait.func (lim *TokenLimiter) AllowN(now time.Time, n int) bool {    return lim.reserveN(context.Background(), now, n)}// AllowNCtx reports whether n events may happen at time now with incoming context.// Use this method if you intend to drop / skip events that exceed the rate.// Otherwise, use Reserve or Wait.func (lim *TokenLimiter) AllowNCtx(ctx context.Context, now time.Time, n int) bool {    return lim.reserveN(ctx, now, n)}

最终调用的都是 reverveN 方法:Ljl28资讯网——每日最新资讯28at.com

func (lim *TokenLimiter) reserveN(ctx context.Context, now time.Time, n int) bool {    // 判断 Redis 健康状态,如果 Redis 故障,则使用进程内限流器    if atomic.LoadUint32(&lim.redisAlive) == 0 {        return lim.rescueLimiter.AllowN(now, n)    }    // 执行限流脚本    resp, err := lim.store.EvalCtx(ctx,        script,        []string{            lim.tokenKey,            lim.timestampKey,        },        []string{            strconv.Itoa(lim.rate),            strconv.Itoa(lim.burst),            strconv.FormatInt(now.Unix(), 10),            strconv.Itoa(n),        })    // redis allowed == false    // Lua boolean false -> r Nil bulk reply    if err == redis.Nil {        return false    }    if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {        logx.Errorf("fail to use rate limiter: %s", err)        return false    }    if err != nil {        logx.Errorf("fail to use rate limiter: %s, use in-process limiter for rescue", err)        // 如果有异常的话,会启动进程内限流        lim.startMonitor()        return lim.rescueLimiter.AllowN(now, n)    }    code, ok := resp.(int64)    if !ok {        logx.Errorf("fail to eval redis script: %v, use in-process limiter for rescue", resp)        lim.startMonitor()        return lim.rescueLimiter.AllowN(now, n)    }    // redis allowed == true    // Lua boolean true -> r integer reply with value of 1    return code == 1}

最后看一下进程内限流的启动与恢复:Ljl28资讯网——每日最新资讯28at.com

func (lim *TokenLimiter) startMonitor() {    lim.rescueLock.Lock()    defer lim.rescueLock.Unlock()    // 需要加锁保护,如果程序已经启动了,直接返回,不要重复启动    if lim.monitorStarted {        return    }    lim.monitorStarted = true    atomic.StoreUint32(&lim.redisAlive, 0)    go lim.waitForRedis()}func (lim *TokenLimiter) waitForRedis() {    ticker := time.NewTicker(pingInterval)    // 更新监控进程的状态    defer func() {        ticker.Stop()        lim.rescueLock.Lock()        lim.monitorStarted = false        lim.rescueLock.Unlock()    }()    for range ticker.C {        // 对 redis 进行健康监测,如果 redis 服务恢复了        // 则更新 redisAlive 标识,并退出 goroutine        if lim.store.Ping() {            atomic.StoreUint32(&lim.redisAlive, 1)            return        }    }}

参考文章:Ljl28资讯网——每日最新资讯28at.com

  • https://juejin.cn/post/7052171117116522504
  • https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673

本文链接:http://www.28at.com/showinfo-26-5770-0.htmlGo-Zero 是如何实现令牌桶限流的?

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 「Go面经」算法 并发模型 缓存落盘 etcd actor模型

下一篇: 阿里云还会继续降价吗?

标签:
  • 热门焦点
  • 7月安卓手机性能榜:红魔8S Pro再夺榜首

    7月份的手机市场风平浪静,除了红魔和努比亚带来了两款搭载骁龙8Gen2领先版处理器的新机之外,别的也想不到有什么新品了,这也正常,通常6月7月都是手机厂商修整的时间,进入8月份之
  • 不容错过的MSBuild技巧,必备用法详解和实践指南

    一、MSBuild简介MSBuild是一种基于XML的构建引擎,用于在.NET Framework和.NET Core应用程序中自动化构建过程。它是Visual Studio的构建引擎,可在命令行或其他构建工具中使用
  • JavaScript学习 -AES加密算法

    引言在当今数字化时代,前端应用程序扮演着重要角色,用户的敏感数据经常在前端进行加密和解密操作。然而,这样的操作在网络传输和存储中可能会受到恶意攻击的威胁。为了确保数据
  • 最“俊美”淘宝卖家,靠直播和短视频圈粉,上架秒光,年销3000万

    来源 | 电商在线文|易琬玉编辑|斯问受访店铺:Ringdoll戒之人形图源:微博@御座的黄山、“Ringdoll戒之人形”淘宝店铺有关外貌的评价,黄山已经听累了。生于1985年的他,哪
  • 新电商三兄弟,“抖快红”成团!

    来源:价值研究所作 者:Hernanderz 随着内容电商的概念兴起,抖音、快手、小红书组成的“新电商三兄弟”成为业内一股不可忽视的势力,给阿里、京东、拼多多带去了巨大压
  • 当家的盒马,加速谋生

    来源 | 价值星球Planet作者 | 归去来自己“当家”的盒马,开始加速谋生了。据盒马官微消息,盒马计划今年开放生鲜供应链,将其生鲜商品送往食堂。目前,盒马在上海已经与
  • 阿里大调整

    来源:产品刘有媒体报道称,近期淘宝天猫集团启动了近年来最大的人力制度改革,涉及员工绩效、层级体系等多个核心事项,目前已形成一个初步的“征求意见版”:1、取消P序列
  • 朋友圈可以修改可见范围了 苹果用户可率先体验

    近日,iOS用户迎来微信8.0.27正式版更新,除了可更换二维码背景外,还新增了多项实用功能。在新版微信中,朋友圈终于可以修改可见范围,简单来说就是已发布的朋友圈
  • 联想小新Pad Pro 12.6将要推出,搭载高通骁龙 870 处理器

    联想小新Pad Pro 12.6将于秋季新品会上推出,官方按照惯例直接在发布会前给出了机型的所有参数。联想小新 Pad Pro 12.6 将搭载高通骁龙 870 处理器,重量为 5
Top