设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间23 现在时间23 缓存数据 好好的睡一觉吧,闭上眼睛做个好梦,明天睁眼又会是美好的一天,晚安好梦。

好好的睡一觉吧,闭上眼睛做个好梦,明天睁眼又会是美好的一天,晚安好梦。

查看: 269|回复: 0

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

[复制链接]

  离线 

TA的专栏

  • 打卡等级:热心大叔
  • 打卡总天数:205
  • 打卡月天数:0
  • 打卡总奖励:3150
  • 最近打卡:2023-08-27 01:47:29
等级头衔

等級:晓枫资讯-上等兵

在线时间
0 小时

积分成就
威望
0
贡献
407
主题
378
精华
0
金钱
4338
积分
811
注册时间
2022-12-23
最后登录
2025-5-31

发表于 2025-5-31 06:54:07 | 显示全部楼层 |阅读模式
常见接口限流算法

今天面试时,面试官对我之前实习时实现的限流功能很感兴趣,发起了夺命连问…
正好趁此机会好好整理一下,很棒。

常用的限流算法


固定窗口

实现思想
固定窗口的实现原理是:在指定周期内累加访问次数,当访问次数达到限制时,触发限流策略。
比如我们限制3s内的请求不超过两次,当第三次访问的时候检测到当前窗口内以处理2次,对本次进行限制。
1.png

优点:

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

  • 限流不平滑,如第2秒到第5秒的窗口内之间可以有3次请求。
代码实现
固定窗口我们可以基于 redis 设置过期时间的 string 实现。当 对 redis 的操作出现 err 时,建议放行,因为限流的目的是
  1. 降低服务器的压力
复制代码
,而不是让服务器“宕机”。
  1. var limit struct {
  2.         count int
  3.         cycle time.Duration
  4. }

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

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

  11.                 // 获取客户端IP
  12.                 clientIP := c.ClientIP()
  13.                 // 不包括参数
  14.                 path := c.Request.URL.Path
  15.                 key := clientIP + path
  16.         
  17.                 valStr, err := rdb.Get(c, key).Result()
  18.                 if err != nil {
  19.                         // 根据业务处理(放行/拦截)
  20.                 }
  21.                 val, _ := strconv.Atoi(valStr)
  22.                 if val >= limit.count {
  23.                         c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
  24.                                 "code": 1,
  25.                                 "msg":  "请求过于频繁",
  26.                         })
  27.                         return
  28.                 }
  29.                 count, err := rdb.Incr(context.Background(), key).Result()
  30.                 if err != nil {
  31.                         // 根据业务处理(放行/拦截)
  32.                 }
  33.                 if count == 1 {
  34.                         err = rdb.Expire(context.Background(), key, limit.cycle).Err()
  35.                         if err != nil {
  36.                                 // 删除key或者重试
  37.                         }
  38.                 }
  39.                 if int(count) > limit.count {
  40.                         c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
  41.                                 "code": 1,
  42.                                 "msg":  "请求过于频繁",
  43.                         })
  44.                         return
  45.                 }
  46.         }
  47. }
复制代码
滑动窗口

实现思想
滑动窗口是指在每一个时间窗口内的次数都不超过限制次数。
还是以3秒内请求不超过两次为例子,当我们每次请求时,统计一下前3秒到现在次数。如果大于等于2次时,则进行拦截。
2.png

优点:

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

  • 实现相对复杂
  • 还是不够平滑。假如我们限制在60s 内请求20次,会存在第一秒内请求了20次,而在后面59秒内都进行拦截的情况。
代码实现
滑动窗口可以基于 reids 的 zset 实现,以请求时的时间戳作为分数。通过当前查询分数区间[ 当前时间戳 - 时间窗口 , 当前时间戳 ),可以快速统计出时间窗口内的次数。下面的代码比固定窗口的代码短的原因是因为直接将 err 忽略了(均不影响限流功能)。
  1. var limit struct {
  2.         count int64
  3.         cycle int64 // 单位s
  4. }

  5. func init() {
  6.         limit.count = 2
  7.         limit.cycle = 3
  8. }

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

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

  14.                 t := time.Now().Unix()
  15.                 has, _ := rdb.Exists(context.Background(), key).Result()
  16.                 count, _ := rdb.ZCount(context.Background(), key, fmt.Sprintf("%d", t-limit.cycle), "+inf").Result()
  17.                 if has == 0 { // 如果是第一次创建,最长时间不超过1小时
  18.                         rdb.Expire(context.Background(), key, 1*time.Hour) // 从功能上来说,此处不管是否设置成功,都不影响限流功能
  19.                 }
  20.                 if count >= limit.count { // 超出次数,限制
  21.                         c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
  22.                                 "code": 1,
  23.                                 "msg":  "请求过于频繁",
  24.                         })
  25.                         return
  26.                 }
  27.                
  28.                 rdb.ZAdd(context.Background(), key, &redis.Z{Score: float64(t), Member: strconv.Itoa(int(t))})
  29.                 // 删除窗口外的数据
  30.                 go func() {
  31.                         memberToRemove, _ := rdb.ZRangeByScore(context.Background(), key, &redis.ZRangeBy{
  32.                                 Max: strconv.Itoa(int(t - limit.cycle)),
  33.                                 Min: "0",
  34.                         }).Result()
  35.                         if len(memberToRemove) > 0 {
  36.                                 rdb.ZRem(context.Background(), key, memberToRemove)
  37.                         }
  38.                 }()
  39.         }
  40. }
复制代码
漏桶算法

实现思想
漏桶算法就像小学的游泳池加水放水问题,不管如何加水,放水的速度都是固定的。
漏桶算法的原理是将请求视为水,漏桶用来存贮这些请求。漏桶有一个固定的容量,并且底部有一个小孔,以固定的速度漏水,如果漏桶已满,超出部分的流量将被丢弃(或排队等待)。
3.png

优点:

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

  • 太平滑了,无法应对突发流量场景。
中间件
go有相关的中间件,何苦自己造轮子。
  1. "go.uber.org/ratelimit"
复制代码
包正是基于漏桶算法实现的。
使用方式:

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

  4.         "go.uber.org/ratelimit"
  5. )

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

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

  14.     // Output:
  15.     // 0 0
  16.     // 1 10ms
  17.     // 2 10ms
  18.     // 3 10ms
  19.     // 4 10ms
  20.     // 5 10ms
  21.     // 6 10ms
  22.     // 7 10ms
  23.     // 8 10ms
  24.     // 9 10ms
  25. }
复制代码
代码实现
如果是以所有的请求为粒度则定义一个全局的 ratelimit 即可。下面是以
  1. ip+接口
复制代码
为粒度的限制,需要定义一个map存放 key 和 与之对应的限流器。
  1. import (
  2.         "github.com/gin-gonic/gin"
  3.         "go.uber.org/ratelimit"
  4.         "sync"
  5.         "time"
  6. )

  7. var limiters sync.Map

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

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

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

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

  26.         }
  27. }
复制代码
令牌桶算法

实现思想
令牌桶(Token Bucket)算法与漏桶十分相似,不过前者是服务端产生“水”,后者是服务端消费“水”。
令牌桶算法是指在固定时间间隔内向“桶”中添加“令牌”,桶满则暂时不放。请求在处理前需要从桶中获取令牌。如果桶中有足够的令牌,请求被处理;否则,请求被拒绝或等待。
4.png

中间件
基于此算法实现的中间件有:
  1. github.com/juju/ratelimit
复制代码
  1. golang.org/x/time/rate
复制代码
等。
下面简单说一下
  1. time/rate
复制代码
的使用。
声明一个限流器
  1. limiter := rate.NewLimiter(10, 2)
复制代码
第一个参数代表每秒向令牌桶中产生多少token。第二个参数代表令牌桶的大小,且初始状态下令牌桶是满的。
消费Token
Wait、WaitN
  1. func (lim *Limiter) Wait(ctx context.Context) (err error)
  2. func (lim *Limiter) WaitN(ctx context.Context, n int) (err error)
复制代码
Wait实际上就是
  1. WaitN(context.Background(),1)
复制代码
。当桶内 Token 数组不足(小于N),那么Wait方法将会阻塞一段时间,直至Token满足条件。如果充足则直接返回。
Allow、AllowN
Allow与Wait十分相似,都是用来消费Token,区别是当桶中Token数量不足时,前者立即返回,后者阻塞至满足条件。
  1. func (lim *Limiter) Allow() bool
  2. 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 方法。
  1. func (lim *Limiter) Reserve() *Reservation
  2. func (lim *Limiter) ReserveN(now time.Time, n int) *Reservation
复制代码
当调用完成后,无论 Token 是否充足,都会返回一个
  1. *Reservation
复制代码
对象。
你可以调用该对象的
  1. Delay()
复制代码
方法, 该方法返回了需要等待的时间。如果等待时间为0,则说明不用等待。必须等到等待时间结束之后,才能进行接下来的工作。
如果不想等待,可以调用
  1. Cancel()
复制代码
方法,该方法会将 Token 归还。
代码实现
下面还是以
  1. Ip + path
复制代码
为粒度进行限制,和令牌桶差不多。
  1. func ratelimitMiddleware() func(gin.Context) {
  2.         return func(c gin.Context) {

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

  6.                 var rl *rate.Limiter
  7.                 if limitersVal, ok := limiters.Load(key); ok {
  8.                         rl = limitersVal.(*rate.Limiter)
  9.                 } else {
  10.                         newLimiter := rate.NewLimiter(1, 10)
  11.                         limiters.Store(key, newLimiter)
  12.                         rl = newLimiter
  13.                         go func(string2 string) {
  14.                                 time.Sleep(1 * time.Second)
  15.                                 limiters.Delete(key)
  16.                         }(key)
  17.                 }
  18.                 if !rl.Allow() {
  19.                         c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
  20.                                 "code": 1,
  21.                                 "msg":  "请求过于频繁",
  22.                         })
  23.                 }
  24.         }
  25. }
复制代码
小结


  • 固定窗口计数器算法:

    • 优点

      • 实现简单,易于理解。
      • 可以精确控制每个窗口期内的请求数量。

    • 缺点

      • 无法应对短时间内的请求高峰,可能导致请求在窗口切换时突然增加,造成瞬时压力。
      • 无法平滑处理请求,可能导致用户体验不佳。


  • 滑动窗口算法:

    • 优点

      • 相对于固定窗口算法,可以更平滑地处理请求,减少瞬时压力。
      • 可以更灵活地应对请求的波动。

    • 缺点

      • 实现相对复杂,需要维护多个计数器。
      • 可能会因为窗口滑动导致计数器更新的开销。


  • 漏桶算法:

    • 优点

      • 实现简单,易于控制数据的输出速率。
      • 可以平滑处理请求,避免瞬时压力。

    • 缺点

      • 无法应对突发请求,可能导致请求长时间等待。
      • 处理速度固定,不够灵活。


  • 令牌桶算法:

    • 优点

      • 可以控制平均传输速率,同时允许一定程度的突发请求。
      • 灵活性高,适用于请求速率不均匀的场景。

    • 缺点

      • 实现相对复杂,需要维护令牌的生成和消耗。
      • 需要合理设置令牌的生成速率和桶的大小,否则可能无法达到预期的限流效果。


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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表