当前位置:网站首页>User defined current limiting annotation

User defined current limiting annotation

2022-07-06 20:34:00 Little happy

Redis Except for caching , Can do a lot of things : Distributed lock 、 Current limiting 、 Handle request interface idempotency ... Too much too much ~

Code :https://github.com/1040580896/rate_limiter

1. preparation

First let's create one Spring Boot engineering , introduce Web and Redis rely on , At the same time, considering that the interface current limit is generally marked by annotation , And annotation is through AOP To analyze , So we need to add AOP Dependence , The final dependencies are as follows :

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

Configuration information

Then prepare one in advance Redis example , Here, after our project is configured , Just configure it Redis The basic information of , as follows :

spring.redis.port=6379
spring.redis.host=120.24.87.xxx
spring.redis.password=asd112211

2. Current limiting notes

Next, we create a current limiting annotation , We divide current limiting into two cases :

  1. Global current limit for the current interface , For example, the interface can be in 1 Visit... In minutes 100 Time .
  2. For a certain IP Current limiting of address , For example, a IP The address can be in 1 Visit... In minutes 100 Time .

In both cases , Let's create an enumeration class :

/** *  Current limiting type  */
public enum LimitType {
    

    /** *  The default current limiting side month , Limit current for a certain interface  */
    Default,
    /** *  For a certain  IP  Carry out current limiting  */
    IP
}

Next, let's create a current limiting annotation :

@Retention(RetentionPolicy.RUNTIME)
@Target({
    ElementType.METHOD})
public @interface RateLimiter {
    

    /** *  Current limiting key, Mainly refers to prefix  * @return */
    String key() default "rate_limit";

    /** *  Current limiting time window  * @return */
    int time() default 60;


    /** *  Current limiting times in the time window  * @return */
    int count() default 100;


    /** *  Type of current limiting  * @return */
    LimitType limitType() default LimitType.Default;

}

The first parameter is current limiting key, This is just a prefix , The future is complete key Is this prefix plus the full path of the interface method , Together, they form current limiting key, This key Will be saved to Redis in .

The other three parameters are easy to understand , I won't say much more .

Okay , Which interface needs current limiting in the future , Just add... On which interface @RateLimiter annotation , Then configure relevant parameters .

3. customized RedisTemplate

My friends know , stay Spring Boot in , We are actually more used to using Spring Data Redis To operate Redis, But by default RedisTemplate There's a little pit , Serialization uses JdkSerializationRedisSerializer, I don't know if my friends have noticed , Directly use this serialization tool to save to Redis Upper key and value Will be inexplicably more prefixes , This leads to errors when you read with commands .

For example, when storing ,key yes name,value yes javaboy, But when you operate on the command line ,get name But you can't get the data you want , The reason is to save to redis after name There are some more characters in front , You can only continue to use RedisTemplate Read it out .

We use it Redis Current limiting will use Lua Script , Use Lua Script time , This will happen , So we need to change it RedisTemplate The serialization scheme of .

*

A little friend may say why not StringRedisTemplate Well ?StringRedisTemplate There really is no problem mentioned above , But the data types it can store are not rich enough , So... Is not considered here .

modify RedisTemplate Serialization scheme , The code is as follows :

@Configuration
public class RedisConfig {
    

    @Bean
    RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){
    

        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        template.setKeySerializer(serializer);
        template.setHashKeySerializer(serializer);

        template.setValueSerializer(serializer);
        template.setHashValueSerializer(serializer);

        return template;
    }


    @Bean
    DefaultRedisScript<Long> limitScript(){
    
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setResultType(Long.class);
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua")));
        return script;
    }
}

4. Development Lua Script

Redis For some atomic operations in, we can use Lua Script to achieve , Want to call Lua Script , We have two different ideas :

  1. stay Redis The server side is well defined Lua Script , Then calculate a hash value , stay Java In the code , Lock which... To execute with this hash value Lua Script .
  2. Directly in Java The code will Lua Well defined script , Then send it to Redis The server executes .

Spring Data Redis Operations are also provided in Lua Script interface , It's more convenient , So here's the second option .

We are resources New under the directory lua Folders are dedicated to storing lua Script , The script is as follows :

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)

This script is actually not difficult , You probably know what to do at a glance .KEYS and ARGV They are all parameters passed in when calling later ,tonumber Is to turn a string into a number ,redis.call Is to implement specific redis Instructions , The specific process is as follows :

  1. First get the incoming key as well as Current limiting count And time time.
  2. adopt get Get this key Corresponding value , This value is how many times this interface can be accessed in the current time window .
  3. If it's a first visit , The result obtained at this time is nil, Otherwise, the result should be a number , So the next step is to judge , If the result is a number , And this number is greater than count, That means the flow limit has been exceeded , Then you can directly return the query result .
  4. If the result is nil, The description is the first visit , Now give the current key Self increasing 1, Then set an expiration time .
  5. Finally, self increment 1 The returned value is OK .

In fact, this paragraph Lua The script is easy to understand .

5. Annotation analysis

Next, we need to customize the section , To parse this annotation , Let's look at the definition of section :

@Aspect
@Component
public class RateLimitAspect {
    

    private static final Logger log = LoggerFactory.getLogger(RateLimitAspect.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();
        // Combine key
        String combineKey = getCombineKey(rateLimiter, jp);

        // call 
        try {
    
            Long number = redisTemplate.execute(redisScript, Collections.singletonList(combineKey), time, count);
            if (number == null || number.intValue() > count) {
    
                // Over current limit threshold 
                log.info(" Current interface to reach the maximum current limit times ");
                throw new RateLimitException(" Too many visits , Please visit later ");
            }
            log.info(" Number of requests in a time window :{}, Current number of requests :{}, The cache  key  by  {}", count, number, combineKey);
        } catch (Exception e) {
    
            throw e;
        }


    }

    /** *  This  key  The number of interface calls is cached in  redis  Medium  key * rate_limit:11.11.11.11-com.th.ratelimit.comtroller.HelloController-hello * rate_limit:com.th.ratelimit.comtroller.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();
    }
}

This section is to intercept all added @RateLimiter Method of annotation , Processing annotations in pre notification .

  1. First get the... In the annotation key、time as well as count Three parameters .
  2. Get a combined key, The so-called combined key, It's in the annotation key attributively , Plus the full path of the method , If it is IP In terms of mode , Just add IP Address . With IP Model as an example , Finally generated key Like this :rate_limit:127.0.0.1-org.javaboy.ratelimiter.controller.HelloController-hello( If not IP Pattern , So the generated key Not included in IP Address ).
  3. The generated key Put it in the set .
  4. adopt redisTemplate.execute Method takes and executes a Lua Script , The first parameter is the object encapsulated by the script , The second parameter is key, Corresponds to... In the script KEYS, Followed by variable length parameters , Corresponds to... In the script ARGV.
  5. take Lua The result of script execution is similar to count Compare , If it is greater than count, It means overload , Just throw an exception .

Okay , It's done .

6. The interface test

Next, let's do a simple test of the interface , as follows :

@RestController
public class HelloController {
    @GetMapping("/hello")
    @RateLimiter(time = 5,count = 3,limitType = LimitType.IP)
    public String hello() {
        return "hello>>>"+new Date();
    }
}

every last IP Address , stay 5 Can only access... In seconds 3 Time .

This can be tested by manually refreshing the browser .

7. Global exception handling

Because the overload is thrown out of the exception , So we also need a global exception handler , as follows :

@RestControllerAdvice
public class GlobalException {
    @ExceptionHandler(ServiceException.class)
    public Map<String,Object> serviceException(ServiceException e) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("status", 500);
        map.put("message", e.getMessage());
        return map;
    }
}

This is a little demo, I won't define entity classes , Direct use Map Come back to JSON 了 .

All right. , Be accomplished .

原网站

版权声明
本文为[Little happy]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/187/202207061228358539.html