SpringCloud Gateway implements current limiting through redis [SpringCloud Series 8]

SpringCloud large-scale series of courses are under production, welcome your attention and comments.
Programmers’ daily CV and bricks, but also know why, this series of courses can help beginners learn SpringBooot project development and SpringCloud microservice series project development

This article is one in a series

  • 1. Basic engineering construction of SpringCloud project [SpringCloud Series 1]
  • 2. SpringCloud integrates Nacos registration center [SpringCloud Series 2]
  • 3. SpringCloud Feign remote call [SpringCloud Series 3]
  • 4. SpringCloud Feign remote calls public class extraction [SpringCloud Series 4]
  • 5. SpringCloud integrates Gateway service gateway [SpringCloud Series 5]
  • 6. Spring Cloud integrates Spring Security authentication [Spring Cloud Series 6]
  • Spring Cloud Gateway Gateway Authentication [Spring Cloud Series 7]

In a system with high concurrency, it is often necessary to limit the current in the system. Common current limiting methods:

  • Hystrix applies thread pool isolation, exceeds the load of the thread pool, and follows the logic of fuse
  • The tomcat container limits its number of threads by
  • Control the flow by the average speed of the time window

Common Current Limiting Algorithms

  • The counter algorithm generally limits the number of requests that can pass in one second
  • Leaky bucket algorithm, there is a container inside the algorithm, which is similar to a funnel used in daily life. When a request comes in, it is equivalent to pouring water into the funnel, and then slowly and uniformly flow out from the lower end. Regardless of the flow rate above, the flow rate below remains constant
  • Token bucket algorithm, the token bucket algorithm is an improvement to the leaky bucket algorithm, the bucket algorithm can limit the rate of request calls, and the token bucket algorithm can limit the average rate of calls while allowing a certain degree of burst calls

    This article implements the current limit of redis without fuse function

1 Configure in the gateway project

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

Implement the KeyResolver interface. UriKeyResolver limits the flow of URI.

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class UriKeyResolver implements KeyResolver {<!-- -->

    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {<!-- -->
        final String path = exchange. getRequest(). getURI(). getPath();
        System.out.println(path);
        return Mono. just(path);
    }
}

2 Configure the limit of the specified interface

For example, I configure the interface access limit of order-service here

server:
  port: 10001
spring:
  application:
    name: '@project.name@'
  redis:
    database: 0 # Redis database index (default is 0)
    host: localhost # Redis server address
    port: 6379 # Redis server connection port
    password: 12345678 # Redis server connection password (default is empty)
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos address
    gateway:
      routes: # Gateway routing configuration
        - id: order-service # Routing id, custom, as long as it is unique
          # uri: http://127.0.0.1:8081 # The destination address of the route http is a fixed address
          uri: lb://order-service
          predicates:
            -Path=/order/**
          filters:
            - name: RequestRateLimiter
              args:
                # How many average requests are processed per second
                redis-rate-limiter.replenishRate: 1
                # Order the maximum number of requests allowed to complete in one second
                redis-rate-limiter.burstCapacity: 1
                # Get the Bean object, the ID of @Bean, the default bean name is the same as the method name.
                key-resolver: "#{@uriKeyResolver}"


  • “#{@uriKeyResolver}” in the key-resolver field corresponds to the UriKeyResolver configured above.
    Then the current limiting rule I configured here is to process 1 average number of requests per second, and then I use postman to access the order interface to query order details

  • The name field must be RequestRateLimiter

Call twice in a row, the first time it can be accessed normally, and the second time a 429 error occurs

3 custom 429 return error

The current limit filter is written in RequestRateLimiterGatewayFilterFactory, and the RequestRateLimiter configured in Gateway is the result of removing the suffix of this filter, so you only need to rewrite this filter.

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.Map;

@Slf4j
@Component
public class GatewayRequestRateLimiterGatewayFilterFactory extends RequestRateLimiterGatewayFilterFactory {<!-- -->

    private final RateLimiter defaultRateLimiter;

    private final KeyResolver defaultKeyResolver;

    public GatewayRequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter, KeyResolver defaultKeyResolver) {<!-- -->
        super(defaultRateLimiter, defaultKeyResolver);
        this.defaultRateLimiter = defaultRateLimiter;
        this.defaultKeyResolver = defaultKeyResolver;
    }

    @Override
    public GatewayFilter apply(Config config) {<!-- -->
        KeyResolver resolver = getOrDefault(config. getKeyResolver(), defaultKeyResolver);
        RateLimiter<Object> limiter = getOrDefault(config. getRateLimiter(), defaultRateLimiter);
        return (exchange, chain) -> resolver. resolve(exchange). flatMap(key -> {<!-- -->
            String routeId = config. getRouteId();
            if (routeId == null) {<!-- -->
                Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
                routeId = route. getId();
            }
            String finalRouteId = routeId;
            return limiter.isAllowed(routeId, key).flatMap(response -> {<!-- -->
                for (Map.Entry<String, String> header : response.getHeaders().entrySet()) {<!-- -->
                    exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());
                }
                if (response. isAllowed()) {<!-- -->
                    return chain. filter(exchange);
                }
                log.warn("Limited flow: {}", finalRouteId);
                ServerHttpResponse httpResponse = exchange. getResponse();
                //Modify the code to 500
                httpResponse.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
                if (!httpResponse.getHeaders().containsKey("Content-Type")) {<!-- -->
                    httpResponse.getHeaders().add("Content-Type", "application/json");
                }
                //The global exception handling cannot be triggered here, return manually
                DataBuffer buffer = httpResponse.bufferFactory().wrap(("{\\
"
                         + " "code": "1414","
                         + " "message": "Server current limit","
                         + " "data": "Server throttling","
                         + " "success": false"
                         + "}").getBytes(StandardCharsets.UTF_8));
                return httpResponse.writeWith(Mono.just(buffer));
            });
        });
    }

    private <T> T getOrDefault(T configValue, T defaultValue) {<!-- -->
        return (configValue != null) ? configValue : defaultValue;
    }
}

Then modify the current limit configuration, modify name: GatewayRequestRateLimiter

server:
  port: 10001
spring:
  application:
    name: '@project.name@'
  redis:
    database: 0 # Redis database index (default is 0)
    host: localhost # Redis server address
    port: 6379 # Redis server connection port
    password: 12345678 # Redis server connection password (default is empty)
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos address
    gateway:
      routes: # Gateway routing configuration
        - id: order-service # Routing id, custom, as long as it is unique
          # uri: http://127.0.0.1:8081 # The destination address of the route http is a fixed address
          uri: lb://order-service
          predicates:
            -Path=/order/**
          filters:
            - name: GatewayRequestRateLimiter
              args:
                # How many average requests are processed per second
                redis-rate-limiter.replenishRate: 1
                # Order the maximum number of requests allowed to complete in one second
                redis-rate-limiter.burstCapacity: 1
                # Get the Bean object, the ID of @Bean, the default bean name is the same as the method name.
                key-resolver: "#{@uriKeyResolver}"


Then test the current limit again

Source code of this project https://gitee.com/android.long/spring-cloud-biglead/tree/master/biglead-api-08-redis-reactive
If you are interested, you can pay attention to the public account biglead, there will be java, Flutter, applet, js, English-related content sharing every week