Spring Cloud 微服务五:Spring cloud gateway限流

前言:在互联网应用中,特别是电商,高并发的场景非常多,比如:秒杀、抢购、双11等,在开始时间点会使流量爆发式地涌入,如果对网络流量不加控制很有可能造成后台实例资源耗尽。限流是指通过指定的策略削减流量,使到达后台实例的请求在合理范围内。本章将介绍spring cloud gateway如何实现限流。

前情回顾请参考:

Spring Cloud 微服务一:Consul注册中心

Spring Cloud 微服务二:API网关spring cloud zuul

Spring Cloud 微服务三: API网关Spring cloud gateway

Spring Cloud 微服务四:熔断器Spring cloud hystrix

  • 限流算法
        主流的限流算法有两种:漏桶(leaky bucket)和令牌桶(token bucket)。漏桶算法 有一个固定容量的桶,对于流入的水无法预计速率,流出的水以固定速率,当水满之后会溢出。

    令牌桶算法,有一个固定容量的桶,桶里存放着令牌(token)。桶最开始是空的,token以一个固定速率向桶中填充,直到达到桶的容量,多余的token会被丢弃。每当一个请求过来时,都先去桶里取一个token,如果没有token的话请求无法通过。

 

         两种算法的最主要区别是令牌桶算法允许一定流量的突发,因为令牌桶算法中取走token是不需要时间的,即桶内有多少个token都可以瞬时拿走。基于这个特点令牌桶算法在互联网企业中应用比较广泛,我们在实现限流的时候也会基于这个算法。

  • gateway如何实现限流
  •  方法1:Spring cloud gateway实现限流的方式主要是通过添加自定义filter来实现,自定义filter需要实现GatewayFilter和Ordered接口。本章将结合开源的Bucket4j来实现,Bucket4j是基于令牌桶算法实现,Bucket4j代码参考:https://github.com/vladimir-bukhtoyarov/bucket4j
      首先修改api-gateway module,pom中添加Bucket4j依赖,最新版本是4.3.0,工程的版本已经在父工程中定义好了
<dependency>
       <groupId>com.github.vladimir-bukhtoyarov</groupId> 
       <artifactId>bucket4j-core</artifactId> 
</dependency>

                第二步,添加filter实现GatewayFilter和Ordered,添加相应的参数,并使用一个ConcurrentHashMap存储ip以及bucket,实现filter方法,对客户端访问ip进行过滤

public class LimitFilter implements GatewayFilter, Ordered {
    private static final Logger logger = LoggerFactory.getLogger(LimitFilter.class);

    private int capacity;
    private int refillTokens;
    private Duration refillDuration;

    public LimitFilter(int capacity, int refillTokens, Duration refillDuration) {
        this.capacity = capacity;
        this.refillTokens = refillTokens;
        this.refillDuration = refillDuration;
    }

    private static final Map<String, Bucket> CACHE = new ConcurrentHashMap<>();

    private Bucket createNewBucket() {
        Refill refill = Refill.greedy(refillTokens, refillDuration);
        Bandwidth limit = Bandwidth.classic(capacity, refill);
        return Bucket4j.builder().addLimit(limit).build();
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
        Bucket bucket = CACHE.computeIfAbsent(ip, k -> createNewBucket());
        logger.info("IP: "+ip+", available tokens :"+bucket.getAvailableTokens());
        if (bucket.tryConsume(1L)) {
            return chain.filter(exchange);
        }
        logger.info("IP: "+ip+", available tokens :"+bucket.getAvailableTokens()+" too many requests");
        exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
        return exchange.getResponse().setComplete();

    }

    @Override
    public int getOrder() {
        return 0;
    }
}

             第三步,添加自定义路由,添加配置类,RouteLocator构造器中添加filter以及相应的地址信息,设置同一ip同时只能访问一次,多余的将被忽略。另外,由于我们在程序中配置了路由,需要将application.yml中的gateway相关属性删除。

@Configuration
public class RouteLocatorConfig {
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        System.out.println("============================RouteLocatorConfig "+ builder.routes());
        return builder
                .routes()
                .route(r -> r.path("/*")
                        .filters(f -> f.filter(new LimitFilter(1,
                                1, Duration.ofSeconds(1))))
                        .uri("http://localhost:10080/")
                        .order(0)
                        .id("user_route"))
                .build();
    }
}

         最后,测试,重启api-gateway,访问http://localhost:8088/users,第一次访问成功,频繁刷新会出现空白页,控制台会输出相关信息

             方法2:使用spring cloud 原生的redis方式

    第一步,搭建redis服务器,具体方法参考redis官网

    第二步,pom中添加redis依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

    自定义resolver

@Configuration
public class CustomResolver {
    @Bean
    public KeyResolver ipKeyResolver(){
        System.out.println("##############ipKeyResolver########################");
        return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
    }
}

  第三步,修改application.yml配置

 routes:
        - id: user_route
          uri: http://localhost:10080
          predicates:
            - Path=/*
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 1
                key-resolver: "#{@ipKeyResolver}

        最后做测试,并使用monitor命令监控redis

posted @ 2019-01-18 10:36  大刀客  阅读(8887)  评论(1编辑  收藏  举报