Spring Boot+Redis implements a simple current limiter example

Spring Boot+Redis implements a simple current limiter, limiting


insert image description here

0. Preface

Use Redis and filters to implement request rate limiting in Spring Boot. The filter will check the request frequency as each request arrives and throttle based on the set threshold. This protects your application from malicious requests or high concurrent requests. Please make appropriate modifications and extensions according to your specific needs and business scenarios.

1. Basic introduction

1. Current limiting scenario
Suppose we have an API interface and need to limit the request frequency of each user within a period of time. For example, only 100 requests are allowed per second and so on.
2. Realize the current limiting logic:
use the counter function of Redis to realize the current limiting algorithm based on the time window. By storing the request counter and expiration time in Redis, the request frequency per unit time can be controlled. In the interface or method that needs to be limited, use Redis atomic operations (such as INCR and EXPIRE) to increase the counter and set the expiration time.
When each request arrives, check whether the value of the counter exceeds the set threshold, and if so, reject the request, otherwise allow the request to continue.

In this article, we implement a lightweight message queue through Spring Boot + Redis.

2. Steps

2.1. Introducing dependencies

<dependencies>
    <!-- Spring Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>

2.2. Configuration file

# Redis连接配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=your_password
spring.redis.database=0

# Redis连接池配置
spring.redis.jedis.pool.max-active=50
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.min-idle=5
spring.redis.jedis.pool.max-wait=-1

In the above configuration, you can modify the following properties according to the actual situation:

  • spring.redis.host: The hostname or IP address of the Redis server.
  • spring.redis.port: The port number of the Redis server.
  • spring.redis.password: The password of the Redis server (if any).
  • spring.redis.database: The index of the Redis database, the default is 0.

In addition, you can also configure the properties of the Redis connection pool to control the behavior of the connection pool. In the example configuration, the following connection pool properties are set:

  • spring.redis.jedis.pool.max-active: The maximum number of active connections in the connection pool.
  • spring.redis.jedis.pool.max-idle: The maximum number of idle connections in the connection pool.
  • spring.redis.jedis.pool.min-idle: The minimum number of idle connections in the connection pool.
  • spring.redis.jedis.pool.max-wait: The maximum waiting time (in milliseconds) for obtaining a connection from the connection pool, -1 means infinite waiting.

If you are using a configuration file in YAML format ( application.yml), you can convert the above configuration to the corresponding format:

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: your_password
    database: 0
  redis.jedis.pool:
    max-active: 50
    max-idle: 10
    min-idle: 5
    max-wait: -1

Please adjust it according to your actual Redis server configuration, and add other relevant configurations as needed, such as timeout settings, SSL configuration, etc.

2.3. Core source code

  1. Implement a request throttling filter :
    Create a javax.servlet.Filterrequest throttling filter that implements the interface. In the filter, use the counter function of Redis to implement the request flow limiting logic.
    In the example, it is a request limiting filter that RequestLimitFilterimplements the interface. javax.servlet.FilterIt uses Redis's counter functionality to implement request throttling logic. When each request arrives, according to the client's IP address as the Redis key, increment the value of the counter and set the expiration time to the specified time window. If the counter exceeds the set threshold (here 100), then return HTTP 429 Too Many Requestsa response.

The example is used RedisTemplate<String, String>to operate Redis, which can be adjusted to the RedisTemplate suitable for your data type and operation method according to your needs.

@Component
public class RequestLimitFilter implements Filter {
    
    

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private static final String REQUEST_LIMIT_PREFIX = "requestLimit:";
    private static final long REQUEST_LIMIT = 100; // 请求限制数量
    private static final long TIME_WINDOW = 60; // 时间窗口(单位:秒)

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
    
    
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String ipAddress = getClientIpAddress(httpRequest);

        String key = REQUEST_LIMIT_PREFIX + ipAddress;
        Long counter = redisTemplate.opsForValue().increment(key, 1);

        if (counter == 1) {
    
    
            redisTemplate.expire(key, TIME_WINDOW, TimeUnit.SECONDS);
        }

        if (counter > REQUEST_LIMIT) {
    
    
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            httpResponse.getWriter().write("请求频率超过限制,请稍后再试!");
            return;
        }

        chain.doFilter(request, response);
    }

  private String getClientIpAddress(HttpServletRequest request) {
    
    
    String ipAddress = request.getHeader("X-Forwarded-For");
    if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) {
    
    
        ipAddress = request.getHeader("Proxy-Client-IP");
    }
    if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) {
    
    
        ipAddress = request.getHeader("WL-Proxy-Client-IP");
    }
    if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) {
    
    
        ipAddress = request.getRemoteAddr();
    }
    return ipAddress;
}
}

Optimized

public class RequestLimitFilter implements Filter {
    
    

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private static final String REQUEST_LIMIT_PREFIX = "requestLimit:";
    private static final long REQUEST_LIMIT = 100; // 请求限制数量
    private static final long TIME_WINDOW = 60; // 时间窗口(单位:秒)

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
    
    
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String ipAddress = getClientIpAddress(httpRequest);

        String key = REQUEST_LIMIT_PREFIX + ipAddress;
        Long counter = redisTemplate.opsForValue().increment(key, 1);

        if (counter == 1) {
    
    
            redisTemplate.expire(key, TIME_WINDOW, TimeUnit.SECONDS);
        }

        if (counter > REQUEST_LIMIT) {
    
    
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            try (PrintWriter writer = httpResponse.getWriter()) {
    
    
                writer.write("请求频率超过限制,请稍后再试!");
            }
            return;
        }

        chain.doFilter(request, response);
    }

    private String getClientIpAddress(HttpServletRequest request) {
    
    
      ...
        return ipAddress;
    }
}

Re-optimize and add Bloom filter

Use Bloom filter to reduce access to Redis: Bloom filter is an efficient probabilistic data structure that can be used to quickly determine whether an element exists in a collection. When limiting the frequency of requests, Bloom filters can be used to reduce access to Redis. Redis operations are performed only when the Bloom filter determines that the request is not a repeated request.

public class RequestLimitFilter implements Filter {
    
    

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private static final String REQUEST_LIMIT_PREFIX = "requestLimit:";
    private static final long REQUEST_LIMIT = 100; // 请求限制数量
    private static final long TIME_WINDOW = 60; // 时间窗口(单位:秒)

    private BloomFilter<String> bloomFilter;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    
    
        // 初始化布隆过滤器
        int expectedInsertions = 1000; // 预期插入数量
        double falsePositiveProbability = 0.01; // 误判率
        bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), expectedInsertions, falsePositiveProbability);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
    
    
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String ipAddress = getClientIpAddress(httpRequest);

        if (bloomFilter.mightContain(ipAddress)) {
    
    
            // 布隆过滤器判断可能是重复请求,直接放行
            chain.doFilter(request, response);
            return;
        }

        String key = REQUEST_LIMIT_PREFIX + ipAddress;
        Long counter;
        boolean isNewKey = false;

        try {
    
    
            counter = redisTemplate.opsForValue().increment(key, 1);
            if (counter == 1) {
    
    
                redisTemplate.expire(key, TIME_WINDOW, TimeUnit.SECONDS);
                isNewKey = true;
            }
        } catch (Exception e) {
    
    
            // 处理Redis操作异常
            // 可以选择记录日志或采取适当的处理措施
            e.printStackTrace();
            chain.doFilter(request, response);
            return;
        }

        if (counter > REQUEST_LIMIT) {
    
    
            if (isNewKey) {
    
    
                // 删除新创建的键,避免无限增长
                redisTemplate.delete(key);
            }
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            try (PrintWriter writer = httpResponse.getWriter()) {
    
    
                writer.write("请求频率超过限制,请稍后再试!");
            }
            return;
        }

        bloomFilter.put(ipAddress); // 将IP地址添加到布隆过滤器
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
    
    
        // 清理资源,如关闭Redis连接等
    }

    private String getClientIpAddress(HttpServletRequest request) {
    
    
        // 获取客户端IP地址的逻辑
        // ...
    }
}

In the above code, we introduced Bloom filter to reduce access to Redis. If the Bloom filter judges that the request may be a duplicate request, it will be released directly without Redis operation. At the same time, we also added handling of Redis operation exceptions, and deleted newly created keys when the current limit exceeds the threshold to avoid infinite growth. Please make appropriate adjustments and improvements according to the actual situation.

  1. Register filter :
    Register the filter in the configuration class of the Spring Boot application so that it can take effect during the request processing.
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    

    @Autowired
    private RequestLimitFilter requestLimitFilter;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
    
        registry.addInterceptor(requestLimitFilter);
    }
}

By adding the filter to addInterceptorsthe method, it will be registered as a global filter for the Spring Boot application and execute throttling logic when requests arrive.

4. Summary

In fact, there are still problems with what we have written above.

  1. If the system is deployed on multiple nodes, consider using a distributed current limiting algorithm, such as token bucket algorithm or leaky bucket algorithm. These algorithms can balance the processing of requests in a distributed environment and guarantee a global request limit.
  2. Configure the parameters of request flow limit, such as request limit number and time window, as parameters that can be adjusted dynamically. These parameters can be managed using annotations or configuration files so that adjustments can be made at runtime without recompiling the code.

5. Reference documents

  1. Spring Data Redis official documentation: https://docs.spring.io/spring-data/redis/docs/current/reference/html/ ↗
    This document provides details on how to use Spring Data Redis for Redis operations in Spring Boot guide. Learn how to configure Redis connections, operate with RedisTemplate, and other advanced features.

6. Redis from entry to proficiency series of articles

Guess you like

Origin blog.csdn.net/wangshuai6707/article/details/132278658