Enterprise Redis + Lua Distributed High Concurrency Current Limiting Practical Combat

Reminder

This article is not a personal test, but has been applied online, custom current-limiting component code. Can be promoted online. There is no need to be afraid of magnitude.

It adopts counter + sliding window to achieve, and draws on the famous Sentinel sliding window idea:

Description of Requirement

Distributed current limiting is performed in accordance with the dimensions of every hour, every minute, and every second.

Show results

First look at the jmeter configuration of the pressure test:

200 threads per second access the current-limiting interface, and the loop continues indefinitely.

The interface current limiting condition set in the background is: 5 times/second, code:

Observe the background output:

I spot-checked a few here, and found that the lock is indeed successfully retrieved 5 times per second. Prove that there is no problem with our current limiting code.

Redis memory usage:

There is only one zset set that takes up space, and the number is 5, which is the number of passes per second. That is to say, if you  set 100w requests per second to pass, then the zset will be as large as 100w. I believe you will not set this up! ! ! ! !

When no access is requested, the zset collection will be cleared after 1 second (don't worry about occupying memory forever):

Code explanation

Realize the idea:

"Counter Current Limit + Sliding Window"

When the current limit condition is hourly or every minute, because the accuracy is not very large, the counter current limit can be used. Advantages: the code is simple, and the redis memory only occupies a string type key.

When the current limit condition is per second. Due to the possibility of instantaneous, double flow entry problems, a sliding window is used.

Code

First define a current limiting interface:

/*** *  * title: 分布式限流器 * * @author HadLuo * @date 2021-5-28 9:45:41 */public interface ClusterRateLimiter {     /***     *      * title: 返回0 表示 限流住了,否则返回剩余次数     *     * @param key:限流标识     * @param limitCount:1个单位时间内允许通过的最大次数     * @return     * @author HadLuo 2021-5-28 9:48:45     */    public Long acquire(String key, Integer limitCount);     /***     *      * title: 返回0 表示 限流住了,否则返回剩余次数     *     * @param key:限流标识     * @param limitCount:单位时间内允许通过的最大次数     * @param interval:单位时间     * @return     * @author HadLuo 2021-5-28 9:48:45     */    public Long acquire(String key, Integer limitCount, Integer interval);}

Counter implementation class:

package com.uc.framework.redis.limit; import java.util.Collections;import org.springframework.beans.factory.InitializingBean;import org.springframework.beans.factory.annotation.Autowired;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 org.springframework.util.StringUtils; /*** *  * title: Redis + Lua 限流实现 计数器限流 * * @author HadLuo * @date 2021-5-28 9:41:19 */public class RedisLuaOfCountedLimiter implements InitializingBean, ClusterRateLimiter {    @Autowired    private RedisTemplate<String, Object> redisTemplate;     DefaultRedisScript<Long> script;     @Override    public void afterPropertiesSet() throws Exception {        // 加载 lua        script = new DefaultRedisScript<>();        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("counter_limit.lua")));        script.setResultType(Long.class);     }     @Override    public Long acquire(String key, Integer limitCount, Integer interval) {        if (StringUtils.isEmpty(key) || limitCount <= 0 || interval <= 0) {            return 0L;        }        return (Long)redisTemplate.execute(script, Collections.singletonList(getKey(key)), limitCount + "",            interval + "");    }     private String getKey(String key) {        return "cluster:limit:" + key;    }     @Override    public Long acquire(String key, Integer limitCount) {        throw new UnsupportedOperationException();    }}

Sliding window implementation class:

package com.uc.framework.redis.limit; import java.util.Collections;import org.springframework.beans.factory.InitializingBean;import org.springframework.beans.factory.annotation.Autowired;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 org.springframework.util.StringUtils; /*** *  * title: Redis + Lua 限流实现 滑动窗口限流 * * @author HadLuo * @date 2021-5-28 9:41:19 */public class RedisLuaOfSlidingLimiter implements InitializingBean, ClusterRateLimiter {    @Autowired    private RedisTemplate<String, Object> redisTemplate;     DefaultRedisScript<Long> script;     @Override    public void afterPropertiesSet() throws Exception {        // 加载 lua        script = new DefaultRedisScript<>();        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("sliding_limit.lua")));        script.setResultType(Long.class);     }     @Override    public Long acquire(String key, Integer limitCount) {        if (StringUtils.isEmpty(key) || limitCount <= 0) {            return 0L;        }        // 1秒 = 1 000 000 000纳秒        final long windowSize = 1000000000L;        // 1000000000        Long ret = (Long)redisTemplate.execute(script, Collections.singletonList(getKey(key)), System.nanoTime() + "",            windowSize + "", limitCount + "");        return ret;    }     private String getKey(String key) {        return "cluster:limit:" + key;    }     @Override    public Long acquire(String key, Integer limitCount, Integer interval) {        throw new UnsupportedOperationException();    }}

Context environment, used to construct the above two classes:

package com.uc.framework.redis.limit; import com.uc.framework.App; public final class LimiterContext {     public enum Typer {        Second, Minute, Hour, Day    }     private static volatile ClusterRateLimiter counter, sliding;     private static final Object LockOfSliding = new Object();    private static final Object LockOfCounter = new Object();     public static boolean acquire(Typer typer, String key, int limitCount) {        if (typer == Typer.Second) {            // 精度高,走滑动窗口            if (sliding == null) {                synchronized (LockOfSliding) {                    if (sliding == null) {                     // 由于RedisLuaOfSlidingLimiter里面注入了RedisTemplate ,所以要注入到spring                        sliding = App.registerBeanDefinition(RedisLuaOfSlidingLimiter.class);                    }                }            }            long ret = sliding.acquire(key, limitCount);            return ret > 0;        }        // 按 每分钟, 每小时 ,每天 限流 走计数器, 不耗资源        if (counter == null) {            synchronized (LockOfCounter) {                if (counter == null) {                    counter = App.registerBeanDefinition(RedisLuaOfCountedLimiter.class);                }            }        }        int segment;        if (typer == Typer.Minute) {            segment = 60; // 分        } else if (typer == Typer.Hour) {            segment = 60 * 60; // 时        } else {            segment = 24 * 60 * 60; // 天        }        long ret = counter.acquire(key, limitCount, segment);        return ret > 0;    }}

We are judging the second-level current limit, we use RedisLuaOfSlidingLimiter sliding window to achieve, otherwise it is RedisLuaOfCountedLimiter counter implementation, both classes use dynamic registration to spring.

App code:

package com.uc.framework; import org.springframework.beans.BeansException;import org.springframework.beans.factory.BeanFactory;import org.springframework.beans.factory.BeanFactoryAware;import org.springframework.beans.factory.NoSuchBeanDefinitionException;import org.springframework.beans.factory.config.BeanDefinition;import org.springframework.beans.factory.support.DefaultListableBeanFactory;import org.springframework.beans.factory.support.GenericBeanDefinition;import org.springframework.core.ResolvableType;import org.springframework.stereotype.Component; import com.google.common.base.Preconditions; /** * App spring 句柄方便获取 *  * @author HadLuo * @date 2020-9-2 11:15:54 */@Componentpublic class App implements BeanFactoryAware {     private static BeanFactory beanFactory;     public static BeanFactory getBeanFactory() {        return beanFactory;    }     @Override    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {        Preconditions.checkNotNull(beanFactory);        App.beanFactory = beanFactory;    }     public static Object getBean(String name) throws BeansException {        // TODO Auto-generated method stub        return beanFactory.getBean(name);    }   // -----------自己实现 其它方法         /***     *      * title: 动态注入spring     *     * @param clazz     * @author HadLuo 2021-5-31 10:42:24     * @param <T>     */    public static <T> T registerBeanDefinition(Class<T> clazz) {        BeanDefinition beanDefinition = new GenericBeanDefinition();        beanDefinition.setBeanClassName(clazz.getName());        ((DefaultListableBeanFactory)beanFactory).registerBeanDefinition(clazz.getName(), beanDefinition);        return beanFactory.getBean(clazz);    } }

RedisTemplate and App configuration:

    @Bean    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();        template.setConnectionFactory(factory);        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();        // key采用String的序列化方式        template.setKeySerializer(stringRedisSerializer);        // hash的key也采用String的序列化方式        template.setHashKeySerializer(stringRedisSerializer);        // value序列化方式采用jackson        template.setValueSerializer(stringRedisSerializer);        // hash的value序列化方式采用jackson        template.setHashValueSerializer(stringRedisSerializer);        // template.afterPropertiesSet();        return template;     }     @Bean    public App app() {        return new App();    }

There are only two lua scripts next. Just put it in the environment resource directory:

Counter counter_limit.lua:

local identify = tostring(KEYS[1])local expireSeconds = tonumber(ARGV[2])local limitTimes = tonumber(ARGV[1])- 当前次数值local times = redis.call("GET", identify)- 不为nil, lua里面nil判断要用falseif times ~= false then  times = tonumber(times)  if times >= limitTimes then  - 大于限流次数,返回限流了    return 0  else    redis.call("INCR", identify)  - 增加次数    return 1  endend -- 不存在的话,设置为1并设置过期时间local flag = redis.call("SETEX", identify, expireSeconds, 1) return 1

The sliding window lua script is more important.

Since the code is a lot of time I spent writing and debugging, it is also applied to the company's online, it belongs to intellectual property rights. It's not easy, you need to spend 5 yuan to buy , thank you for your support here! Nowadays, things with a little quality cost money, so please understand~

If you have questions about purchase or online usage, you can contact me.

In this article, at the bottom of this article, I would like to express appreciation. After the appreciation, please contact me (you can leave a message if you know):

qq: 657455400

wx: hadluo

Appreciation article address (turn to the bottom and click: Appreciation from heart):

Enterprise Redis + Lua Distributed Current Limiting Practical Combat