原文链接:

上一篇文章介绍了 如何实现计数器限流?主要有两种实现方式,分别是固定窗口和滑动窗口,并且分析了 go-zero 采用固定窗口方式实现的源码。

但是采用固定窗口实现的限流器会有两个问题:

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

这篇文章来介绍一下令牌桶算法,可以很好解决以上两个问题。

工作原理

算法概念如下:

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

令牌桶算法既能够将所有的请求平均分布到时间区间内,又能接受服务器能够承受范围内的突发请求,因此是目前使用较为广泛的一种限流算法。

源码实现

源码分析我们还是以 go-zero 项目为例,首先来看生成令牌的部分,依然是使用 Redis 来实现。

// 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 = capacity
end // 上次请求时间戳,如果为 nil 则赋值 0
local last_refreshed = tonumber(redis.call("get", KEYS[2]))
if last_refreshed == nil then
last_refreshed = 0
end // 距离上一次请求的时间跨度
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_tokens
if allowed then
new_tokens = filled_tokens - requested
end // 更新 redis token
redis.call("setex", KEYS[1], ttl, new_tokens)
// 更新 redis 刷新时间
redis.call("setex", KEYS[2], ttl, now) return allowed`

Redis 中主要保存两个 key,分别是 token 数量和刷新时间。

核心思想就是比较两次请求时间间隔内生成的 token 数量 + 桶内剩余 token 数量,和请求量之间的大小,如果满足则允许,否则则不允许。

限流器初始化:

// 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 发生故障了,那么就使用这个,算是一个保障,尽量避免系统被突发流量拖垮。

提供了四个可调用方法:

// 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 方法:

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
}

最后看一下进程内限流的启动与恢复:

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
}
}
}

以上就是本文的全部内容,如果觉得还不错的话欢迎点赞转发关注,感谢支持。


参考文章:

推荐阅读:

go-zero 是如何实现令牌桶限流的?的更多相关文章

  1. coding++:高并发解决方案限流技术-使用RateLimiter实现令牌桶限流-Demo

    RateLimiter是guava提供的基于令牌桶算法的实现类,可以非常简单的完成限流特技,并且根据系统的实际情况来调整生成token的速率. 通常可应用于抢购限流防止冲垮系统:限制某接口.服务单位时 ...

  2. 高并发解决方案限流技术-----使用RateLimiter实现令牌桶限流

    1,RateLimiter是guava提供的基于令牌桶算法的实现类,可以非常简单的完成限流特技,并且根据系统的实际情况来调整生成token的速率.通常可应用于抢购限流防止冲垮系统:限制某接口.服务单位 ...

  3. ASP.NET Core中使用令牌桶限流

    在限流时一般会限制每秒或每分钟的请求数,简单点一般会采用计数器算法,这种算法实现相对简单,也很高效,但是无法应对瞬时的突发流量. 比如限流每秒100次请求,绝大多数的时间里都不会超过这个数,但是偶尔某 ...

  4. Redis令牌桶限流

    一 .场景描述 在开发接口服务器的过程中,为了防止客户端对于接口的滥用,保护服务器的资源, 通常来说我们会对于服务器上的各种接口进行调用次数的限制.比如对于某个 用户,他在一个时间段(interval ...

  5. 【springcloud】2.eureka源码分析之令牌桶-限流算法

    国际惯例原理图 代码实现 package Thread; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomi ...

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

    限流 限流又称为流量控制(流控),通常是指限制到达系统的并发请求数. 我们生活中也会经常遇到限流的场景,比如:某景区限制每日进入景区的游客数量为8万人:沙河地铁站早高峰通过站外排队逐一放行的方式限制同 ...

  7. 令牌桶限流思路分享(PHP+Redis实现机制)

    一 .场景描述 在开发接口服务器的过程中,为了防止客户端对于接口的滥用,保护服务器的资源, 通常来说我们会对于服务器上的各种接口进行调用次数的限制.比如对于某个 用户,他在一个时间段(interval ...

  8. Go 分布式令牌桶限流 + 兜底策略

    上篇文章提到固定时间窗口限流无法处理突然请求洪峰情况,本文讲述的令牌桶线路算法则可以比较好的处理此场景. 工作原理 单位时间按照一定速率匀速的生产 token 放入桶内,直到达到桶容量上限. 处理请求 ...

  9. 使用Redis实现令牌桶算法

    在限流算法中有一种令牌桶算法,该算法可以应对短暂的突发流量,这对于现实环境中流量不怎么均匀的情况特别有用,不会频繁的触发限流,对调用方比较友好. 例如,当前限制10qps,大多数情况下不会超过此数量, ...

  10. ASP.NET Core中使用漏桶算法限流

    漏桶算法是限流的四大主流算法之一,其应用场景各种资料中介绍的不多,一般都是说应用在网络流量控制中.这里举两个例子: 1.目前家庭上网都会限制一个固定的带宽,比如100M.200M等,一栋楼有很多的用户 ...

随机推荐

  1. 2023-04-08:社交网络中的最优邀请策略探究。本文以小红准备开宴会为例,提出一种基于贪心算法和二分查找的解决方案,帮助读者在保证愉悦值不低于k的前提下,最小化宴会的阶层差距。

    2023-04-08:小红有n个朋友, 她准备开个宴会,邀请一些朋友, i号朋友的愉悦值为a[i],财富值为b[i], 如果两个朋友同时参加宴会,这两个朋友之间的隔阂是其财富值差值的绝对值, 宴会的隔 ...

  2. Grafana系列-统一展示-9-Jaeger数据源

    系列文章 Grafana 系列文章 配置 Jaeger data source Grafana内置了对Jaeger的支持,它提供了开源的端到端分布式跟踪.本文解释了针对Jaeger数据源的配置和查询. ...

  3. vue全家桶进阶之路48:Vue3 跨域配置devServer的参数和设置

    devServer 是一个用于配置开发服务器的选项对象.它可以用来配置服务器的各种选项,例如代理,端口号,HTTPS 等. 以下是一些常用的 devServer 参数和设置: port:指定开发服务器 ...

  4. 运行和编译时期资源加载的不同【vue】

    开发语言都有编译和运行两个阶段,很多时候这个也会带来许多bug 如:一个项目在开发阶段测试没有问题,然上线发布后就会有这样那样的问题,譬如说图片的加载,静态数据(js,css,json)读取错误 一 ...

  5. chrome 应用程序无法启动,因为应用程序的并行配置不正确

    win10系统谷歌浏览器无法启动,系统报错,如下图: 解决方案: 步骤1:进入chrome的安装路径C:\Program Files (x86)\Google\Chrome\... ...(找到自己的 ...

  6. 【一步步开发AI运动小程序】十、姿态动作相似度比较

    随着人工智能技术的不断发展,阿里体育等IT大厂,推出的"乐动力"."天天跳绳"AI运动APP,让云上运动会.线上运动会.健身打卡.AI体育指导等概念空前火热.那 ...

  7. < Python全景系列-9 > Python 装饰器:优雅地增强你的函数和类

    欢迎来到我们的系列博客<Python全景系列>第九篇!在这个系列中,我们将带领你从Python的基础知识开始,一步步深入到高级话题,帮助你掌握这门强大而灵活的编程语法.无论你是编程新手,还 ...

  8. ABP - 缓存模块(1)

    1. 与 .NET Core 缓存的关系和差异 ABP 框架中的缓存系统核心包是 Volo.Abp.Caching ,而对于分布式缓存的支持,abp 官方提供了基于 Redis 的方案,需要安装 Vo ...

  9. 如何从零开始构建 API ?

    假设你请承包商从零开始建造一座房子,你肯定期望他们交付最高质量的房子.他们必须通过检查.遵守安全规范并遵循项目中约定的要求.因为建房子可容不得走捷径.如果承包商经常走捷径,他们的声誉会受到影响,从而失 ...

  10. Spring配置动态数据库

    前言 本文主要介绍使用spring boot 配置多个数据库,即动态数据库 开始搭建 首先创建一个SpringWeb项目--dynamicdb(spring-boot2.5.7) 然后引入相关依赖lo ...