常用限流方法总结

本文探讨了在高并发场景下保护系统常用的限流方法,包括熔断、降级和限流。重点介绍了三种限流算法:计数器、漏桶和令牌桶。计数器算法简单但存在突刺现象,漏桶算法能匀速处理请求但无法应对突发流量,而令牌桶算法则在限制平均速率的同时允许一定突发调用。此外,还讨论了集群限流的应用,如使用Redis实现用户或商户访问频率的限制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我在写博客接口的时候一直在思考一个问题,因为我曾经用抓包软件模拟请求去图书馆抢座位,当时一不小心重复访问了上千次,导致本就脆弱的服务当场崩溃(我不确定是不是我干的)。所以思考这个问题:常用的限流设计方法有哪些?

高并发业务场景下,保护系统时,常用的"三板斧"有:"熔断、降级和限流"

降级

降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉,待高峰或者问题解决后再打开限流限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理

熔断

熔断是什么?熔断一般是指依赖的外部接口出现故障的情况断绝和外部接口的关系强调的是服务之间的调用能实现自我恢复的状态

限流

限流的目的是通过对并发访问请求进行限速或者一个时间窗口内的的请求数量进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待

限流又分为 单机下的限流 网关层面的限流,这里先聊一聊单机模式下的限流方法:

  • 计数器

  • 漏桶

  • 令牌桶

计数器算法(固定窗口)

顾名思义,在固定时间窗口内对请求进行计数,与阀值进行比较判断是否需要限流,一旦到了时间临界点,将计数器清零。

采用计数器实现限流有点简单粗暴,一般我们会限制一秒钟的能够通过的请求数,比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。
具体的实现可以是这样的:对于每次服务调用,可以通过 AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。
这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”

优点:简单易懂,实现方便

缺点:1.计数器算法存在“时间临界点”缺陷。例如在第一个窗口末期突然有n(n为上限)个请求,在第二个窗口初期突然有n个请求。则短时间内,实际累计了2n个请求在数据巨大的情况下,极易造成超限,导致系统崩溃。

2.还有一个弊端就是假如在一个窗口期(假设为1s)的前10ms内就已经通过了上限的请求,那么接下来的990ms,只能等待拒绝请求。我们把这种现象称为“突刺现象”

 

2、漏桶算法
为了消除"突刺现象",可以采用漏桶算法实现限流,漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。
不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。
在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池定期从队列中获取请求并执行,可以一次性获取多个并发执行。

优点:漏桶算法是流量最均匀的限流实现方式一般用于流量“整形”。

缺点:无法应对短时间的突发流量。可能会造成大量请求丢失,不适合电商抢购和微博出现热点事件等场景的限流。

// 一个固定大小的桶,请求按照固定的速率流出
// 请求数大于桶的容量,则抛弃多余请求
 
type LeakyBucket struct {
   rate       float64    // 每秒固定流出速率
   capacity   float64    // 桶的容量
   water      float64    // 当前桶中请求量
   lastLeakMs int64      // 桶上次漏水微秒数
   lock       sync.Mutex // 锁
}
 
func (leaky *LeakyBucket) Allow() bool {
   leaky.lock.Lock()
   defer leaky.lock.Unlock()
 
   now := time.Now().UnixNano() / 1e6
   // 计算剩余水量,两次执行时间中需要漏掉的水
   leakyWater := leaky.water - (float64(now-leaky.lastLeakMs) * leaky.rate / 1000)
   leaky.water = math.Max(0, leakyWater)
   leaky.lastLeakMs = now
   if leaky.water+1 <= leaky.capacity {
      leaky.water++
      return true
   } else {
      return false
   }
}
 
func (leaky *LeakyBucket) Set(rate, capacity float64) {
   leaky.rate = rate
   leaky.capacity = capacity
   leaky.water = 0
   leaky.lastLeakMs = time.Now().UnixNano() / 1e6
}

 

算法思想

在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。

我们设置好生成令牌放入桶中的速率,如果桶中令牌数达到上限,就丢弃令牌。桶中令牌的容量上限代表了我们可以处理的突发请求上限。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行

优点:从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。 

参数设置

木桶的容量  - 考虑业务逻辑的资源消耗和机器能承载并发处理多少业务逻辑。

生成令牌的速度 - 太慢的话起不到“攒”令牌应对突发流量的效果。

集群限流

前面讨论的几种算法都属于单机限流的范畴,但是业务需求五花八门,简单的单机限流,根本无法满足他们。
比如为了限制某个资源被每个用户或者商户的访问次数,5s只能访问2次,或者一天只能调用1000次,这种需求,单机限流是无法实现的,这时就需要通过集群限流进行实现。

如何实现?

为了控制访问次数,肯定需要一个计数器,用来记录频率,redis非常的合适(快速,高并发,过期时间)

思路:若要是希望实现同一ip的限流,那么就将redis的key:用户id + 路由 + ip地址 ,val为 记录频率 ,并设置过期时间(例如一分钟)

  1. 当用户调用接口的时候,先查询redis中是否有存在该key,获取该key所对应的value,在后端比较value和预设的频率上限,如果小于预设值,则在原来的基础上incr;如果大于则拒绝
  2. 如果不存在,则重设key,和过期时间

参考:

(3条消息) 常用限流算法的应用场景和实现原理_Seekload的博客-优快云博客

(3条消息) 防止同一IP同一时间段内多次访问_柠檬の夏的博客-优快云博客_限制同一ip访问次数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值