分布式限流

限流常见算法:

  1. 计数器算法

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

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

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

  2. 漏桶算法

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

    不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。

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

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

  3. 令牌桶算法

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

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

    放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。

    实现思路:可以准备一个队列,用来保存令牌,另外通过一个线程池定期生成令牌放到队列中,每来一个请求,就从队列中获取一个令牌,并继续执行。

Guava RateLimiter单机限流

Guava RateLimiter使用的算法是令牌桶算法

引入依赖:

1
2
3
4
5
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.0-jre</version>
</dependency>

实现类似抢购的逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@Slf4j
public class LimiterController {

    // 创建可放2令牌的桶且每秒放2令牌,0.5秒放1令牌
    private RateLimiter rateLimiter = RateLimiter.create(10);

    @GetMapping("/guava")
    public void guava(){

        // 每次获取一个令牌
        boolean tryAcquire = rateLimiter.tryAcquire(1);
        if (tryAcquire){
            log.info("抢到了!");
            // 扣库存
            // 下单
            // ...
        }else {
            log.info("抢购失败");
        }
    }
}

因为用于限流的RateLimiter组件是存在于内存里的,所以是单机版限流。需要注意的是,Guava RateLimiter为了应对突发流量,允许令牌多余限定的数量,所以可能会出现超卖现象,需要后续再次进行确认,这里只能起到过滤掉大部分请求的功能。

分布式限流框架Sentinel

首先下载Sentinel控制台:

1
https://github.com/alibaba/Sentinel/releases

启动控制台:

1
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.0.jar

登录http://127.0.0.1:8080/访问Sentinel控制台,默认用户名密码都是sentinel

引入Sentinel依赖:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<dependencies>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.2.4.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    </dependencies>
</dependencyManagement>

增加Spring Boot项目配置:

1
2
3
spring.application.name=limiter
# 配置Sentinel 控制台的地址
spring.cloud.sentinel.transport.dashboard=localhost:8080

增加方法:

1
2
3
4
@GetMapping("/sentinel")
public String sentinel(){
    return "sentinel";
}

启动项目,访问http://127.0.0.1:8082/sentinel后,刷新Sentinel 控制台,可以看到其中增加了项目的列表。在簇点链路中出现了url为sentinel的资源。可对其进行流控、降级、热点、授权等操作。

流控

可以对url设置包括QPS线程数在内的阈值类型来进行阈值限制。

比如,如果设置QPS上限为1(每秒只处理一个请求),不停的访问http://127.0.0.1:8082/sentinel这个地址,有部分请求会直接失败,显示Blocked by Sentinel (flow limiting)。说明流控策略生效。且配置或修改规则后,程序不需重启即可生效。

设置自定义限流

请求被限流后,默认返回给用户的信息是Blocked by Sentinel(flow limiting),如果不想显示这个默认信息,可以自定义限流逻辑并返回给用户:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Slf4j
@Component
public class LimiterBlockHandler implements BlockExceptionHandler {

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
        log.info("被限流了...");
        throw e;
    }
}

此时,当请求被限流时,展示给用户的即是Spring Boot默认支持的500错误。可以配合上Spring Boot全局异常处理针对被限流的请求统一返回格式化的信息给用户。

限流应用内部方法

假设现在有一个内部方法process,想对这个方法进行限流,可以使用@SentinelResource注解进行限流:

1
2
3
4
5
6
7
8
@Service
public class LimitService {

    @SentinelResource("LimiterService.process")
    public String process(){
        return "process";
    }
}

在Controller中引用这个内部方法:

1
2
3
4
5
6
7
8
@Autowired
private LimitService limitService;

@GetMapping("/limitInternalMethod")
public String limitInternalMethod(){
    limitService.process();
    return "limitInternalMethod";
}

启动应用后,访问http://127.0.0.1:8082/limitInternalMethod后,可以发现在Sentinel控制台中簇点链路里出现了/limitInternalMethod和其下属的LimiterService.process资源。可以对LimiterService.process即内部方法进行流控限制。限制为1 QPS后,快速刷新接口,即可发现当访问速度过快时,会报错,说明已实现对该内部方法的流控限流。

限流规则持久化

限流的相关规则是存储在被限流的请求所在的应用程序中的内存里的,即当应用程序重启后,相关规则就会消失。为了用于生产环境,需要对限流规则进行持久化存储。

Sentinel使用Nacos存储规则