REDIS teaching basics that beginners can understand - it's so simple to limit current in a time window

Posted by springo on Thu, 20 Jan 2022 18:09:48 +0100

If you don't know Zset (ordered set), you can read my last article:

REDIS basic teaching article that Xiaobai can also understand - friend interview was stopped by skilist jump table

 

The book went on to say that my friend little A children's shoes were finally available and joined A company. The company is well paid, and it is close to where Xiao A lives. Xiao A likes A big man who is willing to take him. Little A is very satisfied with this job. Time passed day by day. One weekend, little A came to my house for dinner At the dinner table, little A shared with me his experience of an accident last week.

Last week, their company had a serious accident. A background service for exporting reports broke down the report data service, resulting in many businesses querying the service. The main reason is that there are too many report data exported, resulting in a long export time. The front end also does not make anti retry restrictions on the Export button. Multiple operators have clicked the Export button several times. In addition, the retry mechanism configured by the background service magnified the traffic several times, and finally brought down the whole report data service. The boss asked Xiao a to put forward a cluster current limiting scheme to prevent such problems next time.

Now little A panicked. It's easy to limit the current of A single machine. It's done with the addition of the Hystrix framework annotation. Or use Sentinel to configure it in the background of Sentinel Dashboard. How to do cluster current limiting? Little A thought hard for A whole morning and didn't come out. Finally, he had to ask the boss for help.

 

Little A: boss, the boss asked me to come up with A cluster current limiting scheme. I was not familiar with this before. I found A pile of websites that repeat the same feeling. It's unreliable. Can you teach me how to do it?

Boss: don't panic, don't panic, it's not difficult. Let me test you first. What are the current limiting algorithms?

Little A: I think it over

Xiao A: Yes, there are three common current limiting algorithms: sliding window algorithm, token bucket algorithm and leaky bucket algorithm.

Boss: Yes, what's the difference between single machine current limit and cluster current limit?

Small A: emmm

Boss: you can think about the difference between cluster and stand-alone program itself.

Xiao A: I see. Single machine current limiting data exists on A single machine and can only be used by one machine. The cluster is distributed with multiple machines. It is necessary for multiple machines to share the same current limiting data in order to ensure the current limiting of multiple machines.

Boss: good. Do you remember the Zset(Sorted Sets) I asked you during the interview? It can easily achieve a sliding time window.

Little A: big man, teach me!

Boss: I won't say much. Now let's get to the point.

 

If you don't know the of Zset data structure, you can take a look at my article first

REDIS basic teaching article that Xiaobai can also understand - friend interview was stopped by skilist jump table

 

First, let's take a look at the lua code that implements the sliding time window (this is the core code that implements the current limit of Redis's sliding time window)

-- Parameters:
-- nowTime current time 
-- windowTime Window time
-- maxCount Maximum times
-- expiredWindowTime Expired window time
-- value Request tag
local nowTime = tonumber(ARGV[1]);
local windowTime = tonumber(ARGV[2]);
local maxCount = tonumber(ARGV[3]);
local expiredWindowTime = tonumber(ARGV[4])
local value = ARGV[5];
local key = KEYS[1];
-- Gets the number of request flags for the current window
local count = redis.call('ZCARD', key)
-- Compare whether the current number of requests is greater than the maximum number of window requests
if count >= maxCount then
    -- If greater than the maximum number of requests
    -- Deleting the expired request flag to free window space is equivalent to sliding the time window forward
    redis.call('ZREMRANGEBYSCORE', key, 0, expiredWindowTime)
    -- Get the number of request flags of the current window again
    local count = redis.call('ZCARD', key)
    -- Extend expiration time
    redis.call('PEXPIRE', key, windowTime + 1000)
    -- Compare whether the released size is less than the maximum number of window requests
    if count < maxCount then
        -- A return of 200 indicates success
        return 200
    else
        -- Returning 500 means failure
        return 500
    end
else
     -- Inserts the access tag for the current access
    redis.call('ZADD', key, nowTime, value)
    -- Extend expiration time
    redis.call('PEXPIRE', key, windowTime + 1000)
    -- A return of 200 indicates success
    return 200
end

Redis receives the request and starts executing lua script. Find the corresponding Zset according to the Key, that is, the time window. Get the number of request tags in the current time window. If it is less than the maximum number of accesses allowed in the maximum window, directly insert the latest request tag and set the score of the tag = 1642403014820 Ms. Extend the expiration time of the window and return success. As shown in the figure below:

 

 

 

 

If the number of request tags in the current time window is obtained, it is greater than or equal to the maximum number of requests allowed in the window. As shown in the following figure, the number of request marks obtained in the current time window is 6, which is greater than the maximum number of requests allowed in the window 5.

 

 

 

 

Delete the expired requests in the window according to the size of the time window. The score of the current request = 1642403014820 MS, 10000 ms of the time window size. The expiration time is 1642403014820 - 10000 = 164240304820; Then delete the node with score < 1642403004820.

After deletion, get the number of request tags in the current window again. You can see that the current number is 1, which is less than the maximum number of requests in the window. Insert the latest request flag score = 1642403014820 ms. Extend the expiration time of the window and return success.

Boss: do you understand now?

Xiao A: I see. To sum up, we use the ready-made data structure Zset (ordered set) in Redis as the time window. The sort value in the collection is the timestamp when the request occurred. When the request occurs,

Count the total number of requests in the time window. If the total number of requests is less than the maximum number of requests allowed in the window, a request flag is inserted, which is equivalent to the number of requests in the window plus one. If the total number of requests is greater than or equal to the maximum number of requests allowed by the window, you need to delete the expired statistics to free up enough space. The deletion method is to calculate the front boundary of the window, that is, the maximum time that has expired. According to this time stamp, then use the native deletion method of Zset  ZREMRANGEBYSCORE key min max  to delete the request mark less than the maximum expiration time. In fact, deleting expired data here is equivalent to sliding forward in the sliding time window. After deletion, count again whether the number of remaining requests in the next window is greater than or equal to the maximum number of requests in the window. If it is greater than, it will directly return failure and tell the client to reject the request. If less than, the request mark score of the current request is the request timestamp of the current request. So far, a current limiting request has been completed. But I don't understand why lua script should be used? Isn't this an increase in maintenance costs?

Boss: good. That's the basic principle. Why use lua script. That is for the execution of multiple commands and current limiting judgment logic. When you execute the command to delete or get the total number, others are also executing, resulting in inaccurate data and failure of current limiting.

Little A: Mm-hmm. got it.

Boss: but this current limit also has shortcomings. For example, it is inappropriate to set a request that allows access to 1 million times in 10 seconds, because there will be 1 million requests in the window, which will consume a lot of memory space. Remember! Don't use it blindly. Consider it comprehensively according to your business volume.

Little A: OK, big man, remember!

 

Boss: after understanding this, let's take a look at how to write JAVA code.

First, we implement the current limiting scheme through Spring AOP and @ CurrentLimiting tag annotation.

    @GetMapping("getId")
    @CurrentLimiting(value = "getId",
            // ErrorCallback is the error callback. Callback is the bean name. Callback Class is the implementation Class
            errorCallback = @ErrorCallback(callback = "redisCurrentLimitingDegradeCallbackImpl", callbackClass = RedisCurrentLimitingErrorCallbackImpl.class)
,
            // DegradeCallback is the degraded callback, callback is the bean name, and callbackclass is the implementation Class
            degradeCallback = @DegradeCallback(callback = "redisCurrentLimitingDegradeCallbackImpl", callbackClass = RedisCurrentLimitingDegradeCallbackImpl.class))
    public Integer getId(){
        return 1;
    }

 

The following describes the AOP facet class:


package com.raiden.redis.current.limiter.aop;
​
import com.raiden.redis.current.limiter.RedisCurrentLimiter;
import com.raiden.redis.current.limiter.annotation.CurrentLimiting;
import com.raiden.redis.current.limiter.annotation.DegradeCallback;
import com.raiden.redis.current.limiter.annotation.ErrorCallback;
import com.raiden.redis.current.limiter.callbock.RedisCurrentLimitingDegradeCallback;
import com.raiden.redis.current.limiter.chain.ErrorCallbackChain;
import com.raiden.redis.current.limiter.info.RedisCurrentLimiterInfo;
import com.raiden.redis.current.limiter.properties.RedisCurrentLimiterProperties;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
​
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
​
​
/**
 * @Created by: Raiden
 * @Descriotion:
 * @Date:Created in 23:51 2020/8/27
 * @Modified By:
 * Current limiting AOP section class
 */
@Aspect
public class RedisCurrentLimitingAspect {
​
    private Map<String, RedisCurrentLimiterInfo> config;
    private ApplicationContext context;
​
    private ConcurrentHashMap<Method, ErrorCallbackChain> errorCallbackChainCache;
​
    private ConcurrentHashMap<Method, RedisCurrentLimitingDegradeCallback> degradeCallbackCache;
​
    public RedisCurrentLimitingAspect(ApplicationContext context, RedisCurrentLimiterProperties properties){
        this.context = context;
        this.config = properties.getConfig();
        this.errorCallbackChainCache = new ConcurrentHashMap<>();
        this.degradeCallbackCache = new ConcurrentHashMap<>();
    }
​
    @Pointcut("@annotation(com.raiden.redis.current.limiter.annotation.CurrentLimiting) || @within(com.raiden.redis.current.limiter.annotation.CurrentLimiting)")
    public void intercept(){}
​
    @Around("intercept()")
    public Object currentLimitingHandle(ProceedingJoinPoint joinPoint) throws Throwable{
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        CurrentLimiting annotation = AnnotationUtils.findAnnotation(method, CurrentLimiting.class);
        if (annotation == null){
            annotation = method.getDeclaringClass().getAnnotation(CurrentLimiting.class);
        }
        String path = annotation.value();
        //If you don't configure resources, just let it go
        //If the current limiting configuration is not found, let it go
        RedisCurrentLimiterInfo info;
        if (path != null && !path.isEmpty() && (info = config.get(path)) != null){
            try {
                //Check whether current limiting is required
                boolean allowAccess = RedisCurrentLimiter.isAllowAccess(path, info.getWindowTime(), info.getWindowTimeUnit(), info.getMaxCount());
                if (allowAccess){
                    return joinPoint.proceed();
                }else {
                    //Get degraded processor
                    RedisCurrentLimitingDegradeCallback currentLimitingDegradeCallback = degradeCallbackCache.get(method);
                    if (currentLimitingDegradeCallback == null){
                        degradeCallbackCache.putIfAbsent(method, getRedisCurrentLimitingDegradeCallback(annotation));
                    }
                    currentLimitingDegradeCallback = degradeCallbackCache.get(method);
                    //Call downgrade callback
                    return currentLimitingDegradeCallback.callback();
                }
            }catch (Throwable e){
                //If an error is reported, give it to the error callback
                ErrorCallbackChain errorCallbackChain = errorCallbackChainCache.get(method);
                if (errorCallbackChain == null){
                    ErrorCallback[] errorCallbacks = annotation.errorCallback();
                    if (errorCallbacks.length == 0){
                        throw e;
                    }
                    //Put error callback cache
                    errorCallbackChainCache.putIfAbsent(method, new ErrorCallbackChain(errorCallbacks, context));
                }
                errorCallbackChain = errorCallbackChainCache.get(method);
                return errorCallbackChain.execute(e);
            }
        }
        return joinPoint.proceed();
    }
​
    private RedisCurrentLimitingDegradeCallback getRedisCurrentLimitingDegradeCallback(CurrentLimiting annotation) throws IllegalAccessException, InstantiationException {
        DegradeCallback degradeCallback = annotation.degradeCallback();
        String callback = degradeCallback.callback();
        if (callback == null || callback.isEmpty()){
            return degradeCallback.callbackClass().newInstance();
        }else {
            return context.getBean(degradeCallback.callback(), degradeCallback.callbackClass());
        }
    }
}
RedisCurrentLimiter Redis time window current limiting execution class:

package com.raiden.redis.current.limiter;
​
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
​
import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
​
​
/**
 * @Created by: Raiden
 * @Descriotion:
 * @Date:Created in 23:51 2020/8/27
 * @Modified By:
 */
public final class RedisCurrentLimiter {
​
    private static final String CURRENT_LIMITER = "CurrentLimiter:";;
​
    private static String ip;;
​
    private static RedisTemplate redis;
​
    private static ResourceScriptSource resourceScriptSource;
​
    protected static void init(RedisTemplate redisTemplate){
        if (redisTemplate == null){
            throw new NullPointerException("The parameter cannot be null");
        }
        try {
            ip = Inet4Address.getLocalHost().getHostAddress().replaceAll("\\.", "");
        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        }
        redis = redisTemplate;
        //lua files are stored in the redis folder under the resources directory
        resourceScriptSource = new ResourceScriptSource(new ClassPathResource("redis/redis-current-limiter.lua"));
    }
​
    public static boolean isAllowAccess(String path, int windowTime, TimeUnit windowTimeUnit, int maxCount){
        if (redis == null){
            throw new NullPointerException("Redis is not initialized !");
        }
        if (path == null || path.isEmpty()){
            throw new IllegalArgumentException("The path parameter cannot be empty !");
        }
        //Get key
        final String key = new StringBuffer(CURRENT_LIMITER).append(path).toString();
        //Get current timestamp
        long now = System.currentTimeMillis();
        //Gets the window time and converts it to milliseconds
        long windowTimeMillis = windowTimeUnit.toMillis(windowTime);
        //Call lua script and execute
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        //Set the return type to Long
        redisScript.setResultType(Long.class);
        //Set lua script source code
        redisScript.setScriptSource(resourceScriptSource);
        //Execute lua script
        Long result = (Long) redis.execute(redisScript, Arrays.asList(key), now, windowTimeMillis, maxCount, now - windowTimeMillis, createValue(now));
        //Get return value
        return result.intValue() == 200;
    }
​
    private static String createValue(long now){
        return new StringBuilder(ip).append(now).append(Math.random() * 100).toString();
    }
}
RedisCurrentLimiterConfiguration Redis sliding time window current limiting configuration class:

package com.raiden.redis.current.limiter.config;
​
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import com.raiden.redis.current.limiter.RedisCurrentLimiterInit;
import com.raiden.redis.current.limiter.aop.RedisCurrentLimitingAspect;
import com.raiden.redis.current.limiter.common.PublicString;
import com.raiden.redis.current.limiter.properties.RedisCurrentLimiterProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
​
/**
 * @Created by: Raiden
 * @Descriotion:
 * @Date:Created in 23:51 2020/8/27
 * @Modified By:
 */
@EnableConfigurationProperties(RedisCurrentLimiterProperties.class)
// Judge whether there is redis in the configuration current-limiter. Load only when enabled = true
@ConditionalOnProperty(
        name = {"redis.current-limiter.enabled"}
)
public class RedisCurrentLimiterConfiguration {
​
    /**
     * AOP Section configuration
     * @param properties
     * @param context
     * @return
     */
    @Bean
    public RedisCurrentLimitingAspect redisCurrentLimitingAspect(RedisCurrentLimiterProperties properties, ApplicationContext context){
        return new RedisCurrentLimitingAspect(context, properties);
    }
​
    /**
     * RedisTemplate to configure
     * @param redisConnectionFactory
     * @return
     */
    @Bean(PublicString.REDIS_CURRENT_LIMITER_REDIS_TEMPLATE)
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // Set serialization
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(
                Object.class);
        ObjectMapper om = new ObjectMapper()
                .registerModule(new ParameterNamesModule())
                .registerModule(new Jdk8Module())
                .registerModule(new JavaTimeModule());
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // Configure redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        RedisSerializer stringSerializer = new StringRedisSerializer();
        // key serialization
        redisTemplate.setKeySerializer(stringSerializer);
        // value serialization
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // Hash key serialization
        redisTemplate.setHashKeySerializer(stringSerializer);
        // Hash value serialization
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
​
    /**
     * Generate Redis sliding time window current limiter initialization class
     * @param redisTemplate
     * @return
     */
    @Bean
    @ConditionalOnBean(name = PublicString.REDIS_CURRENT_LIMITER_REDIS_TEMPLATE)
    public RedisCurrentLimiterInit redisCurrentLimiterInit(RedisTemplate<String, Object> redisTemplate){
        return new RedisCurrentLimiterInit(redisTemplate);
    }
}

Boss: Well, I've shown you the actual combat of Java code. Have you learned it now? If you feel good, please give me a compliment.

 

Code github address:

https://github.com/RaidenXin/redis-current-limiter.git

Students in need can pull down and have a look.

 

This article is from the blog Park and written by: Raiden_xin For reprint, please indicate the original link: https://www.cnblogs.com/Raiden-xin/p/15815644.html

 

Turn https://www.cnblogs.com/Raiden-xin/p/15815644.html

Topics: Java