• notice
  • Congratulations on the launch of the Sought Tech site

An annotation implements the current limit of the Redis interface!

In addition to caching, Redis can also do many things: distributed locks, current limiting, and processing request interface idempotency. . . too much too much~

Today, I want to talk to my friends about using Redis to handle interface current limiting. This is also the knowledge point involved in the recent TienChin project. I will bring it out and talk to you about this topic, and I will talk about it in the video later.

1. Preparations

First, we create a Spring Boot project, introduce Web and Redis dependencies, and consider that the interface current limit is generally marked by annotations, and annotations are parsed through AOP, so we also need to add AOP dependencies. 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>

Then prepare a Redis instance in advance. After our project is configured, we can directly configure the basic information of Redis, as follows:

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123

Well, the preparation work is in place.

2. Current Limit Notes

Next we create a current limit annotation, we divide the current limit into two cases:

  1. Global current limit for the current interface, for example, the interface can be accessed 100 times in one minute.

  2. Current limit for a certain IP address, for example, a certain IP address can be accessed 100 times within 1 minute.

For both cases, we create an enumeration class:

public enum LimitType {
    /**
     * Default policy to limit traffic globally
     */
    DEFAULT,
    /**
     * Limit current based on requester IP
     */
    IP}

Next we create the current limiting annotation:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
    /**
     * Current limiting key
     */
    String key() default "rate_limit:";

    /**
     * Current limiting time, in seconds
     */
    int time() default 60;

    /**
     * Current limit times
     */
    int count() default 100;

    /**
     * Current limiting type
     */
    LimitType limitType() default LimitType.DEFAULT;
}

The first parameter is the current-limiting key, which is just a prefix. In the future, the complete key will be this prefix plus the complete path of the interface method to form the current-limiting key, which will be stored in Redis.

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

Well, which interface needs to limit the current in the future, just add @RateLimiterannotations , and then configure the relevant parameters.

3. Customize RedisTemplate

Friends know that in Spring Boot, we are actually more accustomed to using Spring Data Redis to operate Redis, but the default RedisTemplate has a small pit, that is, the serialization uses JdkSerializationRedisSerializer, I don’t know if you have noticed it, directly Using this serialization tool in the future, the keys and values stored on Redis will be inexplicably more prefixed, which may cause errors when you read them with commands.

For example, when storing, the key is name and the value is javaboy, but when you operate on the command line, get nameyou cannot get the data you want. The reason is that there are some more characters in front of the name after saving to redis. At this time, you can only Continue to use RedisTemplate to read it out.

When we use Redis for current limiting, we will use Lua scripts. When using Lua scripts, the situation mentioned above will occur, so we need to modify the serialization scheme of RedisTemplate.

Some friends may say why not use StringRedisTemplate? StringRedisTemplate does not have the above-mentioned problems, but the data types it can store are not rich enough, so it is not considered here.

Modify the RedisTemplate serialization scheme, the code is as follows:

@Configurationpublic class RedisConfig {
   @Bean
   public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
       RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
       redisTemplate.setConnectionFactory(connectionFactory);
       // 使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化)
       Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
       ObjectMapper om = new ObjectMapper();
       om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
       om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
       jackson2JsonRedisSerializer.setObjectMapper(om);
       redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
       redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
       redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
       redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
       return redisTemplate;
   }}

In fact, there is nothing to say about this. We use the default jackson serialization method in Spring Boot to solve the key and value.

4. Develop Lua scripts

In fact, I mentioned this in the previous vhr video. We can implement some atomic operations in Redis with the help of Lua scripts. To call Lua scripts, we have two different ideas:

  1. Define the Lua script on the Redis server, and then calculate a hash value. In the Java code, the Lua script to be executed is locked by this hash value.

  2. The Lua script is defined directly in the Java code, and then sent to the Redis server for execution.

Spring Data Redis also provides an interface for operating Lua scripts, which is quite convenient, so we use the second solution here.

We create a new lua folder in the resources directory to store lua scripts. The script content is as follows:

local key = KEYS[1]local count = tonumber(ARGV[1])local time = tonumber(ARGV[2])local current = redis.call('get', key)if current and tonumber(current) > count then
   return tonumber(current)endcurrent = redis.call('incr', key)if tonumber(current) == 1 then
   redis.call('expire', key, time)endreturn tonumber(current)

This script is actually not difficult, you probably know what it is used for at a glance. KEYS and ARGV are both parameters that are passed in when calling for a while, tonumber is to convert a string to a number, and redis.call is to execute a specific redis command. The specific process is as follows:

  1. First, get the incoming key and the current-limiting count and time.

  2. The value corresponding to this key is obtained through get, and this value is how many times this interface can be accessed in the current time window.

  3. If it is the first visit, the result obtained at this time is nil, otherwise the result obtained should be a number, so the next step is to judge, if the result obtained is a number, and the number is greater than count, then Indicates that the traffic limit has been exceeded, then the query result can be returned directly.

  4. If the result obtained is nil, it means that it is the first access. At this time, the current key is incremented by 1, and then an expiration time is set.

  5. Finally, return the value after incrementing by 1.

In fact, this Lua script is easy to understand.

Next we load this Lua script in a Bean, as follows:

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

Okay, our Lua script is now ready.

5. Annotation parsing

Next, we need to customize the aspect to parse this annotation. Let's take a look at the definition of the aspect:

@Aspect@Componentpublic class RateLimiterAspect {
   private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
   @Autowired
   private RedisTemplate<Object, Object> redisTemplate;
   @Autowired
   private RedisScript<Long> limitScript;
   @Before("@annotation(rateLimiter)")
   public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
       String key = rateLimiter.key();
       int time = rateLimiter.time();
       int count = rateLimiter.count();
       String combineKey = getCombineKey(rateLimiter, point);
       List<Object> keys = Collections.singletonList(combineKey);
       try {
           Long number = redisTemplate.execute(limitScript, keys, count, time);
           if (number==null || number.intValue() > count) {
               throw new ServiceException("访问过于频繁,请稍候再试");
           }
           log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), key);
       } catch (ServiceException e) {
           throw e;
       } catch (Exception e) {
           throw new RuntimeException("服务器限流异常,请稍候再试");
       }
   }
   public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
       StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
       if (rateLimiter.limitType() == LimitType.IP) {
           stringBuffer.append(IpUtils.getIpAddr(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest())).append("-");
       }
       MethodSignature signature = (MethodSignature) point.getSignature();
       Method method = signature.getMethod();
       Class<?> targetClass = method.getDeclaringClass();
       stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
       return stringBuffer.toString();
   }}

This aspect is to intercept all @RateLimiterannotated methods and process the annotations in the pre-notification.

  1. First get the three parameters of key, time and count in the annotation.

  2. To obtain a combined key, the so-called combined key is based on the key attribute of the annotation, plus the full path of the method, and if it is in IP mode, plus the IP address. Taking IP mode as an example, the final generated key looks like this: rate_limit:127.0.0.1-org.javaboy.ratelimiter.controller.HelloController-hello(If it is not in IP mode, the generated key does not contain IP address).

  3. Put the generated key into the collection.

  4. To execute a Lua script through the redisTemplate.execute method, the first parameter is the object encapsulated by the script, the second parameter is the key, which corresponds to the KEYS in the script, followed by a variable-length parameter, which corresponds to the ARGV in the script .

  5. Compare the result of the execution of the Lua script with the count, if it is greater than the count, it means overload, just throw an exception.

OK, you're done.

6. Interface test

Next, we will perform a simple test of the interface, as follows:

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

Each IP address can only be accessed 3 times within 5 seconds.

This can be tested by manually refreshing the browser.

7. Global exception handling

Since an exception is thrown when overloaded, we also need a global exception handler, as follows:

@RestControllerAdvicepublic 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 small demo, I will not define the entity class, and use Map to return JSON directly.

Alright, you're done.

Finally, let's take a look at the test effect when overloaded:

Well, this is how we use Redis for throttling.

Song Ge is recording a video of the TienChin project recently. The content of this article is also part of the video of the TienChin project. The TienChin project adopts the Spring Boot+ Vue3 technology stack, which will involve various interesting technologies. Come and do a completion rate with Song Ge. Over 90% of projects.


Tags

Technical otaku

Sought technology together

Related Topic

0 Comments

Leave a Reply

+