Spring Boot implements current limiting annotations

You can also use RateLimiter

In high-concurrency systems, the three ways to protect the system are: caching, downgrading, and current limiting.

The purpose of current limiting is to protect the system by limiting the rate of concurrent access requests or the number of requests within a time window. Once the rate limit is reached, service can be denied, queued, or waited.

1. Current limiting type enumeration class

/**

 * Current limiting type

 * @author ss_419

 */

public enum LimitType {

    /**

     * Default current limiting policy, restricting current for a certain interface

     */

    DEFAULT,

    /**

     * Limit current for a certain IP

     */

    IP

}

2. Customized current limiting annotations

/**

 * @author ss_419

 */

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface RateLimiter {

    /**

     * Current-limiting key, mainly refers to the prefix

     * @return

     */

    String key() default "rate_limit:";

 

    /**

     * Number of current limits within the time window

     * @return

     */

    int count() default 100;

 

    /**

     * Current limiting type

     * @return

     */

    LimitType limitType() default LimitType.DEFAULT;

    /**

     * Current limiting time window

     * @return

     */

    int time() default 60;

}

3. Current limiting lua script

1. Since we use Redis for current limiting, we need to introduce the maven dependency of Redis and the dependency of aop.

<!-- aop dependency -->

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-aop</artifactId>

</dependency>

<!-- redis dependency -->

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

2. Configure redis and lua scripts

@Configuration

public class RedisConfig {

 

    @Bean

    RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory factory) {

        RedisTemplate<Object, Object> template = new RedisTemplate<>();

        template.setConnectionFactory(factory);

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        template.setKeySerializer(jackson2JsonRedisSerializer);

        template.setValueSerializer(jackson2JsonRedisSerializer);

        template.setHashKeySerializer(jackson2JsonRedisSerializer);

        template.setHashValueSerializer(jackson2JsonRedisSerializer);

 

        return template;

    }

 

    /**

     * Read lua script

     * @return

     */

    @Bean

    DefaultRedisScript<Long> limitScript() {

        DefaultRedisScript<Long> script = new DefaultRedisScript<>();

        script.setResultType(Long.class);

        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua")));

        return script;

    }

}

Through the Lua script, it is judged based on the key value cached in Redis whether the number of visits exceeds the current limit within the current limit time (also the expiration time of the key). If not, the number of visits + 1 will be returned, and true will be returned. If it is exceeded, false will be returned.
limit.lua:

local key = KEYS[1]

local time = tonumber(ARGV[1])

local count = tonumber(ARGV[2])

local current = redis.call('get', key)

if current and tonumber(current) > count then

    return tonumber(current)

end

current = redis.call('incr', key)

if tonumber(current) == 1 then

    redis.call('expire', key, time)

end

return tonumber(current)

4. Current limiting aspect processing class

1. Use the Lua script we just used to determine whether the current limit has been exceeded. If the current limit is exceeded, a custom exception will be returned, and then the exception will be caught in the global exception and JSON data will be returned.

2. Based on the annotation parameters, determine the current limiting type and splice the cache key value

package org.pp.ratelimiter.aspectj;

import org.aspectj.lang.JoinPoint;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Before;

import org.aspectj.lang.reflect.MethodSignature;

import org.pp.ratelimiter.annotation.RateLimiter;

import org.pp.ratelimiter.enums.LimitType;

import org.pp.ratelimiter.exception.RateLimitException;

import org.pp.ratelimiter.utils.IpUtils;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.core.script.RedisScript;

import org.springframework.stereotype.Component;

import org.springframework.web.context.request.RequestContextHolder;

import org.springframework.web.context.request.ServletRequestAttributes;

 
import java.lang.reflect.Method;

import java.util.Collections;

@Aspect

@Component

public class RateLimiterAspect {

private static final Logger logger = LoggerFactory.getLogger(RateLimiterAspect.class);
 
    @Autowired

    RedisTemplate<Object, Object> redisTemplate;
 
    @Autowired

    RedisScript<Long> redisScript;
 
    @Before("@annotation(rateLimiter)")

    public void before(JoinPoint jp, RateLimiter rateLimiter) throws RateLimitException {

        int time = rateLimiter.time();

        int count = rateLimiter.count();

        String combineKey = getCombineKey(rateLimiter, jp);

        try {

            Long number = redisTemplate.execute(redisScript, Collections.singletonList(combineKey), time, count);

            if (number == null || number.intValue() > count) {

                //Exceeded current limit threshold

                logger.info("The current interface has reached the maximum number of traffic limits");

                throw new RateLimitException("The access is too frequent, please visit later");

            }

            logger.info("The number of requests within a time window: {}, the current number of requests: {}, the cached key is {}", count, number, combineKey);

        } catch (Exception e) {

            throw e;

        }

    }

 

    /**

     * This key is actually the key that caches the number of interface calls in redis

     * rate_limit:11.11.11.11-org.javaboy.ratelimit.controller.HelloController-hello

     * rate_limit:org.javaboy.ratelimit.controller.HelloController-hello

     * @param rateLimiter

     * @param jp

     * @return

     */

    private String getCombineKey(RateLimiter rateLimiter, JoinPoint jp) {

        StringBuffer key = new StringBuffer(rateLimiter.key());

        if (rateLimiter.limitType() == LimitType.IP) {

            key.append(IpUtils.getIpAddr(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()))

                    .append("-");

        }

        MethodSignature signature = (MethodSignature) jp.getSignature();

        Method method = signature.getMethod();

        key.append(method.getDeclaringClass().getName())

                .append("-")

                .append(method.getName());

        return key.toString();

    }

}

5. Use and testing

@RestController

public class HelloController {
    /**

     * Within 10 seconds of current limit, this interface can be accessed 3 times

     * @return

     */

    @GetMapping("/hello")

    @RateLimiter(time = 10, count = 3)

    public Map<String, Object> hello() {

        Map<String, Object> map = new HashMap<>();

        map.put("status", 200);

        map.put("message", "Hello RateLimiter");

        return map;

    }
 
}

An exception will be reported if the number of visits exceeds 3 within ten seconds

The data in redis is increased by 1 for each access

When the number of visits exceeds 3, the current flow will be limited

The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Java skill treeAnnotationsAnnotation-based unit testing 139023 people are learning the system