Skip to content

Rate_Limiter

睿驰 edited this page Mar 16, 2021 · 13 revisions

高并发架构设计有三大法宝:限流、熔断和降级。今天和大家谈谈限流算法的几种实现方式,本文所说的限流并非是Nginx层面的限流,而是业务代码中的逻辑限流。

限流算法实现

常见的限流算法有:计数器、令牌桶、漏桶。

1、计数器算法

采用计数器实现限流有点简单粗暴,一般我们会限制一秒钟的能够通过的请求数,比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。

具体的实现可以是这样的:对于每次服务调用,可以通过AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。

这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”

2、漏桶算法

漏桶算法 可以查看 维基百科上的定义 Leaky bucket

为了消除"突刺现象",可以采用漏桶算法实现限流。漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。

因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。

在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池(ScheduledExecutorService)来定期从队列中获取请求并执行,可以一次性获取多个并发执行。

这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。

3、令牌桶

关于令牌桶算法 可以查阅维基百科上这篇文章 Token Bucket Algorithm,有兴趣可以看看。

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

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

具体实现

juice框架提供了多种限流方式,参见juice-ratelimiter模块,提供3种开箱即用限流方案:

  • SemaphoreBasedRateLimiter(单机版限流,基于java Semaphore);
  • RedisRateLimiter(令牌桶算法实现);
  • RedisConcurrentRateLimiter(控制全局请求并发量)。

使用教程

首先,pom.xml添加juice-spring-boot-starter依赖:

<dependency>
    <groupId>io.infinityclub</groupId>
    <artifactId>juice-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

application.yml增加redis配置:

# redis
spring:
  redis:
    database: 0
    host: localhost
    password: admin
    port: 6379
    ssl: false
    lettuce:
      pool:
        max-wait: 5000ms
        maxActive: 20
        maxIdle: 8
        minIdle: 1
    timeout: 3000ms

juice-spring-boot-starter会自动装配juice-ratelimiter模块并向Spring容器中注入 RedisRateLimiterRedisConcurrentRateLimiter,开发直接使用这2个类即可,代码如下:

import juice.contracts.ResultDTO;
import juice.ratelimiter.internal.RedisRateLimiter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * 商品秒杀 【接口限流示例】
 * @author Ricky Fung
 */
@RestController
@RequestMapping("/api/sec-kill")
public class SecKillController {
    private final Logger LOG = LoggerFactory.getLogger(this.getClass());

    @Resource
    private RedisRateLimiter redisRateLimiter;

    @GetMapping("/submit")
    public ResultDTO submit(@RequestParam("productId") Integer productId) {
        boolean success = redisRateLimiter.tryAcquire();
        if (!success) {
            LOG.info("商品秒杀-提交请求, productId:{} 超出最大处理能力", productId);
            return ResultDTO.invalidParam("服务忙......");
        }
        //业务逻辑处理
        
        LOG.info("商品秒杀-提交请求, productId:{} 秒杀成功", productId);
        return ResultDTO.ok();
    }
}

相关资料

Clone this wiki locally