SpringBoot + Redis implements API interface current limiting

Table of contents

Learn about Redis

Requirements & Why interface current limiting is needed

Implementation plan

Option 1: Fixed time period

Idea:

accomplish:

(1) Interceptor

 (2) AOP

defect:

Option 2: Sliding window

Idea:

accomplish:


Learn about Redis

Redis (Remote Dictionary Server) is an open source high-performance key-value storage database. It supports a variety of data structures, including String, Hash, List, Set, Sorted Set, etc. Features of Redis include:

  1. Memory storage: Redis stores data in memory, so the read and write speed is very fast, and it is suitable for scenarios with high performance requirements.

  2. Persistence: Redis supports persistence to save data in memory to the hard disk so that the data can be restored after the server is restarted.

  3. Diverse data structures: Redis not only supports simple key-value pair storage, but also supports rich data structures, such as lists, sets, ordered sets, etc., giving it more functions and uses.

  4. High concurrency: Redis is a single-threaded model that supports high concurrency by using asynchronous I/O and non-blocking I/O.

  5. Multi-language support: Redis supports clients in multiple programming languages, such as Java, Python, C#, etc., making it easier for developers to use it on different platforms.

  6. Publish/Subscribe: Redis supports the publish/subscribe mode, allowing the client to subscribe to one or more channels and receive messages from the corresponding channels.

  7. Transaction support: Redis supports transactions, which can execute multiple commands in a transaction and ensure the atomicity of these commands.

Because Redis has high performance, flexible data structure, and rich functions, it is widely used in various application scenarios such as caching, message queues, counters, real-time rankings, and session management.

Requirements & Why interface current limiting is needed

Requirement: For the same IP, the number of interface requests in 60 seconds cannot exceed 10,000 times

Interface current limiting is to protect the system and services and prevent system overload, performance degradation or even crash due to too many requests. The following are several main reasons for interface current limiting:

  1. Prevent malicious attacks: Interface flow limiting can prevent malicious users or attackers from attacking the system through a large number of requests, protecting the stability and security of the system.

  2. Protect system resources: For some computing-intensive or resource-consuming interfaces, limiting the frequency of requests can avoid excessive consumption of server resources and ensure the processing of other normal requests.

  3. Avoid the avalanche effect: When a service is unavailable or the response time is too long, if there are no current limiting measures, a large number of requests may flood into the backend, causing more requests to fail and creating an avalanche effect.

  4. Improve system performance: Current limiting can control the number of concurrent requests and prevent excessive requests from causing excessive server load, thereby improving the overall performance and response speed of the system.

  5. Provide fair resource allocation: Through current limiting, fair allocation can be achieved among different users or different service requests, preventing certain requests from occupying too many resources and affecting other requests.

To sum up, interface current limiting is an important means to protect the system and improve performance, which is especially important for high-concurrency systems. By properly setting the current limiting policy, resource utilization and system stability can be effectively balanced, providing a better user experience.

Implementation plan

Option 1: Fixed time period

Idea:

When the user accesses the interface for the first time, a key containing the user IP and the interface method name is set in Redis. The value is initialized to 1 (indicating the first visit to the current interface), and the expiration time of the key is set at the same time. (60 seconds), as long as the Redis key has not expired, the value will be incremented by 1 for each access. Before the user accesses the interface, he will first get the current number of interface accesses from Redis. If the number of accesses is found to be greater than the specified number of times (more than 10,000 times), an identification of interface access failure is returned to the user.

accomplish:

(1) Interceptor

1. Add Redis dependency: First pom.xmladd Spring Data Redis dependency in the file

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

2. Configure Redis connection information: Configure Redis connection information in application.propertiesor , including host, port, password, etc.application.yml

3. Create a current-limiting interceptor: Create a current-limiting interceptor in the project to limit the interface current of the user IP. Interceptors can implement HandlerInterceptorinterfaces and override preHandlemethods to perform current limiting logic.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;

public class RateLimitInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String ipAddress = getIpAddress(request);
        String uri = request.getRequestURI().replace("/","_");
        String key = "apiVisits:" + uri + ":" + ipAddress;

        // 判断是否已经达到限流次数
        String value = redisTemplate.opsForValue().get(key);
        // key 不存在,则是第一次请求设置过期时间
        if(StringUtils.isBlank(value)){
            redisTemplate.opsForValue().increment(key, 1);
            redisTemplate.expire(key, time, TimeUnit.SECONDS);
            return true;
        }
        if (value != null && Integer.parseInt(value) > 10) {
            response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
            return false;
        }

        // 未达到限流次数,自增
        redisTemplate.opsForValue().increment(key, 1);
        return true;
    }

    private String getIpAddress(HttpServletRequest request) {
        // 从请求头或代理头中获取真实IP地址
        String ipAddress = request.getHeader("X-Forwarded-For");
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddr();
        }
        return ipAddress;
    }
}

4. Register interceptor: Register a custom current limiting interceptor in the configuration class.

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private RateLimitInterceptor rateLimitInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(rateLimitInterceptor).addPathPatterns("/**");
    }
}

 (2) AOP

Implemented in the form of annotations + aspects, just add annotations to the API that needs to be limited.

1. Create annotations

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentLimiting {
    
    /**
     * 缓存key
     */
    String key() default "apiVisits:";

    /**
     * 限流时间,单位秒
     */
    int time() default 5;

    /**
     * 限流次数
     */
    int count() default 10;
}

2. Create AOP aspects

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class CurrentLimitingAspect {

    private final RedisTemplate redisTemplate;

    /**
     * 带有注解的方法之前执行
     */
    @SuppressWarnings("unchecked")
    @Before("@annotation(currentLimiting)")
    public void doBefore(JoinPoint point, CurrentLimiting currentLimiting) throws Throwable {
        int time = currentLimiting.time();
        int count = currentLimiting.count();
        // 将接口方法和用户IP构建Redis的key
        String key = getCurrentLimitingKey(currentLimiting.key(), point);
        
        // 判断是否已经达到限流次数
        String value = redisTemplate.opsForValue().get(key);
        if (value != null && Integer.parseInt(value) > count) {
            log.error("接口限流,key:{},count:{},currentCount:{}", key, count, value);
            throw new RuntimeException("访问过于频繁,请稍后再试!");
        }
        // 未达到限流次数,自增
        redisTemplate.opsForValue().increment(key, 1);
        // key 不存在,则是第一次请求设置过期时间
        if(StringUtils.isBlank(value)){
            redisTemplate.expire(key, time, TimeUnit.SECONDS);
        }
    }

    /**
     * 组装 redis 的 key
     */
    private String getCurrentLimitingKey(String prefixKey,JoinPoint point) {
        StringBuilder sb = new StringBuilder(prefixKey);
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        sb.append( Utils.getIpAddress(request) );
        
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        return sb.append("_").append( targetClass.getName() )
                .append("_").append(method.getName()).toString();
    }
}

defect:

When the interface is accessed at 10:00, a piece of data is written to Reids with the number of accesses being 1. Suddenly it is accessed 9999 times at 10:59, then redis expires, and it is accessed 9999 times at 11:00. This is a problem. That is, it was visited 9999+9999 times between 10:59 and 11:00. Therefore, limiting traffic in a fixed time period may not work, and there will be a critical point when Reids expire, resulting in a large number of user visits.

Option 2: Sliding window

Idea:

Since the time of option 1 is fixed, we can change the fixed time period to dynamic, that is, every time the user accesses the interface, the time point (timestamp) of the current user's access is recorded, and the number of users in the previous minute is calculated. The total number of times the interface has been accessed. If the total number of times is greater than the current limit number, the user is not allowed to access the interface. This ensures that the number of user visits at any time will not exceed 10,000 times.

accomplish:

1. Create annotations

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentLimiting {
    
    /**
     * 缓存key
     */
    String key() default "apiVisits:";

    /**
     * 限流时间,单位秒
     */
    int time() default 5;

    /**
     * 限流次数
     */
    int count() default 10;
}

2. Create AOP aspects

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class CurrentLimitingAspect {

    private final RedisTemplate redisTemplate;

    /**
     * 带有注解的方法之前执行
     */
    @SuppressWarnings("unchecked")
    @Before("@annotation(currentLimiting)")
    public void doBefore(JoinPoint point, CurrentLimiting currentLimiting) throws Throwable {
        int time = currentLimiting.time();
        int count = currentLimiting.count();
        // 将接口方法和用户IP构建Redis的key
        String key = getCurrentLimitingKey(currentLimiting.key(), point);
        
        // 使用Zset的 score 设置成用户访问接口的时间戳
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();

        // 当前时间戳
        long currentTime = System.currentTimeMillis();
        zSetOperations.add(key, currentTime, currentTime);

        // 设置过期时间防止key不消失
        redisTemplate.expire(key, time, TimeUnit.SECONDS);

        // 移除 time 秒之前的访问记录,动态时间段
        zSetOperations.removeRangeByScore(key, 0, currentTime - time * 1000);
        
        // 获得当前时间窗口内的访问记录数
        Long currentCount = zSetOperations.zCard(key);
        // 限流判断
        if (currentCount > count) {
            log.error("接口限流,key:{},count:{},currentCount:{}", key, count, currentCount);
            throw new RuntimeException("访问过于频繁,请稍后再试!");
        }
    }

    /**
     * 组装 redis 的 key
     */
    private String getCurrentLimitingKey(String prefixKey,JoinPoint point) {
        StringBuilder sb = new StringBuilder(prefixKey);
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        sb.append( Utils.getIpAddress(request) );
        
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        return sb.append("_").append( targetClass.getName() )
                .append("_").append(method.getName()).toString();
    }
}

Guess you like

Origin blog.csdn.net/weixin_43820024/article/details/131912773