golang常见接口限流算法的实现

 更新时间:2025年03月03日 09:39:38   作者:木易小熙  
本文主要介绍了golang常见接口限流算法的实现,包含固定窗口、滑动窗口、漏桶和令牌桶,具有一定的参考价值,感兴趣的可以了解一下

常见接口限流算法

今天面试时,面试官对我之前实习时实现的限流功能很感兴趣,发起了夺命连问…

正好趁此机会好好整理一下,很棒。

常用的限流算法

固定窗口

实现思想

固定窗口的实现原理是:在指定周期内累加访问次数,当访问次数达到限制时,触发限流策略。

比如我们限制3s内的请求不超过两次,当第三次访问的时候检测到当前窗口内以处理2次,对本次进行限制。

在这里插入图片描述

优点:

  • 实现简单。可以直接通过 redis 的 string 类型实现。将客户端 ip + 访问端口作为 key,访问次数作为 value,并设置过期时间。

缺点:

  • 限流不平滑,如第2秒到第5秒的窗口内之间可以有3次请求。

代码实现

固定窗口我们可以基于 redis 设置过期时间的 string 实现。当 对 redis 的操作出现 err 时,建议放行,因为限流的目的是降低服务器的压力,而不是让服务器“宕机”。

var limit struct {
	count int
	cycle time.Duration
}

func init() {
	limit.count = 2
	limit.cycle = 3 * time.Second
}

func ratelimit() func(c *gin.Context) {
	return func(c *gin.Context) {

		// 获取客户端IP
		clientIP := c.ClientIP()
		// 不包括参数
		path := c.Request.URL.Path
		key := clientIP + path
        
		valStr, err := rdb.Get(c, key).Result()
		if err != nil {
			// 根据业务处理(放行/拦截)
		}
		val, _ := strconv.Atoi(valStr)
		if val >= limit.count {
			c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
				"code": 1,
				"msg":  "请求过于频繁",
			})
			return
		}
		count, err := rdb.Incr(context.Background(), key).Result()
		if err != nil {
			// 根据业务处理(放行/拦截)
		}
		if count == 1 {
			err = rdb.Expire(context.Background(), key, limit.cycle).Err()
			if err != nil {
				// 删除key或者重试
			}
		}
		if int(count) > limit.count {
			c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
				"code": 1,
				"msg":  "请求过于频繁",
			})
			return
		}
	}
}

滑动窗口

实现思想

滑动窗口是指在每一个时间窗口内的次数都不超过限制次数。

还是以3秒内请求不超过两次为例子,当我们每次请求时,统计一下前3秒到现在次数。如果大于等于2次时,则进行拦截。

在这里插入图片描述

优点:

  • 可以保证任意时间窗口内的请求次数都不超过限制。

缺点:

  • 实现相对复杂

  • 还是不够平滑。假如我们限制在60s 内请求20次,会存在第一秒内请求了20次,而在后面59秒内都进行拦截的情况。

代码实现

滑动窗口可以基于 reids 的 zset 实现,以请求时的时间戳作为分数。通过当前查询分数区间[ 当前时间戳 - 时间窗口 , 当前时间戳 ),可以快速统计出时间窗口内的次数。下面的代码比固定窗口的代码短的原因是因为直接将 err 忽略了(均不影响限流功能)。

var limit struct {
	count int64
	cycle int64 // 单位s
}

func init() {
	limit.count = 2
	limit.cycle = 3
}

func ratelimit() func(c *gin.Context) {
	return func(c *gin.Context) {

		clientIp := c.ClientIP()
		path := c.Request.URL.Path
		key := clientIp + path

		t := time.Now().Unix()
		has, _ := rdb.Exists(context.Background(), key).Result()
		count, _ := rdb.ZCount(context.Background(), key, fmt.Sprintf("%d", t-limit.cycle), "+inf").Result()
		if has == 0 { // 如果是第一次创建,最长时间不超过1小时
			rdb.Expire(context.Background(), key, 1*time.Hour) // 从功能上来说,此处不管是否设置成功,都不影响限流功能
		}
		if count >= limit.count { // 超出次数,限制
			c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
				"code": 1,
				"msg":  "请求过于频繁",
			})
			return
		}
		
		rdb.ZAdd(context.Background(), key, &redis.Z{Score: float64(t), Member: strconv.Itoa(int(t))})
		// 删除窗口外的数据
		go func() {
			memberToRemove, _ := rdb.ZRangeByScore(context.Background(), key, &redis.ZRangeBy{
				Max: strconv.Itoa(int(t - limit.cycle)),
				Min: "0",
			}).Result()
			if len(memberToRemove) > 0 {
				rdb.ZRem(context.Background(), key, memberToRemove)
			}
		}()
	}
}

漏桶算法

实现思想

漏桶算法就像小学的游泳池加水放水问题,不管如何加水,放水的速度都是固定的。

漏桶算法的原理是将请求视为水,漏桶用来存贮这些请求。漏桶有一个固定的容量,并且底部有一个小孔,以固定的速度漏水,如果漏桶已满,超出部分的流量将被丢弃(或排队等待)。

在这里插入图片描述

优点:

  • 平滑限制请求的处理速度,避免瞬间请求过多导致系统崩溃,通过桶的大小和漏出速率灵活时应不同场景。

缺点:

  • 太平滑了,无法应对突发流量场景。

中间件

go有相关的中间件,何苦自己造轮子。"go.uber.org/ratelimit" 包正是基于漏桶算法实现的。

使用方式:

  • 通过 ratelimit.New 创建限流器对象,参数为每秒允许的请求数(RPS)。
  • 使用 Take() 方法来获取限流许可,该方法会阻塞请求知道满足限速要求。

官方示例:

import (
	"fmt"
	"time"

	"go.uber.org/ratelimit"
)

func main() {
    rl := ratelimit.New(100) // 每秒多少次

    prev := time.Now()
    for i := 0; i < 10; i++ {
        now := rl.Take()	// 平均时间
        fmt.Println(i, now.Sub(prev))
        prev = now
    }

    // Output:
    // 0 0
    // 1 10ms
    // 2 10ms
    // 3 10ms
    // 4 10ms
    // 5 10ms
    // 6 10ms
    // 7 10ms
    // 8 10ms
    // 9 10ms
}

代码实现

如果是以所有的请求为粒度则定义一个全局的 ratelimit 即可。下面是以ip+接口为粒度的限制,需要定义一个map存放 key 和 与之对应的限流器。

import (
	"github.com/gin-gonic/gin"
	"go.uber.org/ratelimit"
	"sync"
	"time"
)

var limiters sync.Map

func ratelimitMiddleware() func(c *gin.Context) {
	return func(c *gin.Context) {

		clientIp := c.ClientIP()
		path := c.Request.URL.Path
		key := clientIp + path

		var rl ratelimit.Limiter
		if limiterVal, ok := limiters.Load(key); ok {
			rl = limiterVal.(ratelimit.Limiter)
		} else {
			newLimiter := ratelimit.New(1)	// 每秒只能请求1次
			limiters.Store(key, newLimiter)
			rl = newLimiter
			go func(string) { // 简易回收key,防止limiters 无限增大
				time.Sleep(1 * time.Hour)
				limiters.Delete(key)
			}(key)
		}

		rl.Take() // 超过请求次数会进行阻塞,直到放行或放弃请求

	}
}

令牌桶算法

实现思想

令牌桶(Token Bucket)算法与漏桶十分相似,不过前者是服务端产生“水”,后者是服务端消费“水”。

令牌桶算法是指在固定时间间隔内向“桶”中添加“令牌”,桶满则暂时不放。请求在处理前需要从桶中获取令牌。如果桶中有足够的令牌,请求被处理;否则,请求被拒绝或等待。

在这里插入图片描述

中间件

基于此算法实现的中间件有:github.com/juju/ratelimitgolang.org/x/time/rate等。

下面简单说一下 time/rate 的使用。

声明一个限流器

limiter := rate.NewLimiter(10, 2)

第一个参数代表每秒向令牌桶中产生多少token。第二个参数代表令牌桶的大小,且初始状态下令牌桶是满的。

消费Token

Wait、WaitN

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

Wait实际上就是WaitN(context.Background(),1)。当桶内 Token 数组不足(小于N),那么Wait方法将会阻塞一段时间,直至Token满足条件。如果充足则直接返回。

Allow、AllowN

Allow与Wait十分相似,都是用来消费Token,区别是当桶中Token数量不足时,前者立即返回,后者阻塞至满足条件。

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

Allow 实际上是 AllowN(time.Now(),1)。

AllowN方法表示,截止到当前某一时刻,目前桶中数目是否至少为n个,满足则返回true,同时从桶中消费 n 个 token。反之返回不消费 Token,false。

通常应对这样的线上场景,如果请求速率过快,就直接丢弃到某些请求。

Reserver、ReserveN

官方提供的限流器有阻塞等待式的 Wait,也有直接判断方式的 Allow,还有提供了自己维护预留式的,但核心的实现都是下面的 reserveN 方法。

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

当调用完成后,无论 Token 是否充足,都会返回一个 *Reservation 对象。

你可以调用该对象的 Delay() 方法, 该方法返回了需要等待的时间。如果等待时间为0,则说明不用等待。必须等到等待时间结束之后,才能进行接下来的工作。

如果不想等待,可以调用 Cancel() 方法,该方法会将 Token 归还。

代码实现

下面还是以Ip + path 为粒度进行限制,和令牌桶差不多。

func ratelimitMiddleware() func(gin.Context) {
	return func(c gin.Context) {

		client := c.ClientIP()
		path := c.Request.URL.Path
		key := client + path

		var rl *rate.Limiter
		if limitersVal, ok := limiters.Load(key); ok {
			rl = limitersVal.(*rate.Limiter)
		} else {
			newLimiter := rate.NewLimiter(1, 10)
			limiters.Store(key, newLimiter)
			rl = newLimiter
			go func(string2 string) {
				time.Sleep(1 * time.Second)
				limiters.Delete(key)
			}(key)
		}
		if !rl.Allow() {
			c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
				"code": 1,
				"msg":  "请求过于频繁",
			})
		}
	}
}

小结

  • 固定窗口计数器算法:
    • 优点
      • 实现简单,易于理解。
      • 可以精确控制每个窗口期内的请求数量。
    • 缺点
      • 无法应对短时间内的请求高峰,可能导致请求在窗口切换时突然增加,造成瞬时压力。
      • 无法平滑处理请求,可能导致用户体验不佳。
  • 滑动窗口算法:
    • 优点
      • 相对于固定窗口算法,可以更平滑地处理请求,减少瞬时压力。
      • 可以更灵活地应对请求的波动。
    • 缺点
      • 实现相对复杂,需要维护多个计数器。
      • 可能会因为窗口滑动导致计数器更新的开销。
  • 漏桶算法:
    • 优点
      • 实现简单,易于控制数据的输出速率。
      • 可以平滑处理请求,避免瞬时压力。
    • 缺点
      • 无法应对突发请求,可能导致请求长时间等待。
      • 处理速度固定,不够灵活。
  • 令牌桶算法:
    • 优点
      • 可以控制平均传输速率,同时允许一定程度的突发请求。
      • 灵活性高,适用于请求速率不均匀的场景。
    • 缺点
      • 实现相对复杂,需要维护令牌的生成和消耗。
      • 需要合理设置令牌的生成速率和桶的大小,否则可能无法达到预期的限流效果。

到此这篇关于golang常见接口限流算法的实现的文章就介绍到这了,更多相关golang 接口限流 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

相关文章

  • 详解Go如何基于现有的context创建新的context

    详解Go如何基于现有的context创建新的context

    在 Golang 中,context 包提供了创建和管理上下文的功能,那么在GO语言中如何基于现有的context创建新的context,下面小编就来和大家详细聊聊
    2024-01-01
  • Go错误处理之errors包全面解析与最佳实践

    Go错误处理之errors包全面解析与最佳实践

    标准库errors包是Go提供的基础错误处理工具,它不仅允许我们创建和包装错误,还支持丰富的功能,下面小编将从基础到高级,带你深入掌握 errors 包的使用,快跟随小编一起学习起来吧
    2026-03-03
  • 深入浅出Go:掌握基础知识的关键要点

    深入浅出Go:掌握基础知识的关键要点

    Go是一种开源的编程语言,由Google开发,它具有简洁、高效、并发性强的特点,适用于构建可靠的、高性能的软件系统,本文将介绍Go的基础知识,需要的朋友可以参考下
    2023-10-10
  • 一文带你探索Golang计时器的奥秘

    一文带你探索Golang计时器的奥秘

    在 Golang 中,计时器(timer)是一种常见的工具,用于定期执行某个任务或者在指定时间后触发某个事件。本文将深入探讨 Golang 计时器的实现原理和使用方法,帮助大家更好地理解和应用计时器
    2023-05-05
  • Go语言实现本地缓存的策略详解

    Go语言实现本地缓存的策略详解

    今天给大家分享的是Go语言本地缓存的一些内容,主要是结合bigcache和fastcache两个优秀的开源代码库,总结一些设计思路和感悟,文章通过代码示例介绍的非常详细,需要的朋友可以参考下
    2023-07-07
  • GO语言内存逃逸和GC机制的实现

    GO语言内存逃逸和GC机制的实现

    本文深入解析Go语言的内存管理机制,重点介绍内存逃逸和GC机制,包括指针逃逸、接口类型逃逸等7种典型场景,下面就来详细的介绍一下
    2026-06-06
  • 详解Go语言如何利用高阶函数写出优雅的代码

    详解Go语言如何利用高阶函数写出优雅的代码

    高阶函数(Hiher-order Function)定义为:满足下列条件之一的函数:接收一个或多个函数作为参数;返回值是一个函数。本文为大家介绍了如何利用高阶函数写出优雅的代码,希望对大家有所帮助
    2023-01-01
  • Go 1.22对net/http包的路由增强功能详解

    Go 1.22对net/http包的路由增强功能详解

    Go 1.22 版本对 net/http 包的路由功能进行了增强,引入了方法匹配(method matching)和通配符(wildcards)两项新功能,本文将给大家详细的介绍一下Go 1.22对net/http包的路由增强功能,需要的朋友可以参考下
    2024-02-02
  • 浅谈用Go构建不可变的数据结构的方法

    浅谈用Go构建不可变的数据结构的方法

    这篇文章主要介绍了用Go构建不可变的数据结构的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-09-09
  • go开源Hugo站点渲染之模板词法解析

    go开源Hugo站点渲染之模板词法解析

    这篇文章主要为大家介绍了go开源Hugo站点渲染之模板词法解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-02-02

最新评论