redis分布式限流

背景说明

近期由于项目中调用第三方接口,第三方限制QPS为5,所以在我们系统中需要进行限制,在分布式环境下,首先想到了用redis,当然,redis也提供了现成的限流工具,但是并未用他的那个,而是自己设计了一个工具类

思路说明

1、初始化限流工具,指定QPS数量

2、在redis中设定全局执行状态位

3、获得执行权的线程进行计算后的休眠时间

4、执行目标方法

5、监控执行结果,将结果返回调用

代码实现

工具类代码


import org.example.function.ExcuteMethodParent;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.StaticScriptSource;

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;

public class RLimitUtils {

    private static ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(8);

    private String taskListKey ;

    /**
     * redis 工具类
     */
    private RedisTemplate redisTemplate;


    /**
     * 窗口期数量
     */
    private Integer allTaskNum;

    /**
     * 窗口期数量
     */
    private Integer windowNum;

    /**
     * 窗口期单位
     */
    private TimeUnit timeUnit;

    /**
     * 窗口期毫秒数
     */
    private Long windowsStampNum;

    /**
     * 每个任务之间休眠的数量
     */
    private Long sleepTimePerTask;

    /**
     * redis中的是否可执行状态
     */
    private String taskExcuteFlagKey;

    private AtomicBoolean activeFlag = new AtomicBoolean(false);

    /**
     * 执行结果数据
     */
    public static Map<String,String> taskResultMap = new ConcurrentHashMap<>();

    /**
     * 方法队列
     */
    public static Map<String, ExcuteMethodParent> functionMap = new ConcurrentHashMap<>();

    /**
     * @Param allTaskNum 窗口期内所有可执行任务数 5
     * @Param windowNum 窗口期数值 例如 1 s
     * @Param timeUnit 窗口期单位 s
     * @Param redisTemplate redis的实体类
     * @Param taskListKey redis 中队列名称
     */
    public RLimitUtils(Integer allTaskNum, Integer windowNum, TimeUnit timeUnit, RedisTemplate redisTemplate,String taskListKey){
        this.allTaskNum = allTaskNum;
        this.windowNum = windowNum;
        this.timeUnit = timeUnit;
        this.redisTemplate = redisTemplate;
        this.windowsStampNum = replaceWindowNumToWindowStampNum(windowNum,timeUnit);
        this.sleepTimePerTask = windowsStampNum / allTaskNum;
        this.taskListKey = taskListKey;
        this.taskExcuteFlagKey = taskListKey+"_excuteFlag";
        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(taskExcuteFlagKey, 1);
        //System.out.println("设置标识符:"+aBoolean);
    }
    /**
     * 计算频率数值
     */
    private Long replaceWindowNumToWindowStampNum(Integer windowNum,TimeUnit timeUnit){
        if (timeUnit.equals(TimeUnit.SECONDS)){
            return windowNum.longValue() * 1000;
        }
        if (timeUnit.equals(TimeUnit.MINUTES)){
            return windowNum.longValue() * 1000 * 60;
        }
        if (timeUnit.equals(TimeUnit.HOURS)){
            return windowNum.longValue() * 1000 * 60 * 60;
        }
        if (timeUnit.equals(TimeUnit.DAYS)){
            return windowNum.longValue() * 1000 * 60 * 24;
        }
        return windowNum.longValue();
    }

    /**
     * 外部使用的入口
     */
    public String addFunction(Object expressConfig,ExcuteMethodParent function){
        String functionKey = Thread.currentThread().getName() +":"+ System.currentTimeMillis();
        // System.out.println("放入了key:"+functionKey);
        functionMap.put(functionKey,function);
        redisTemplate.opsForList().rightPush(taskListKey, functionKey);
        // 激活监控
        if (!activeFlag.get()) {
            synchronized (activeFlag) {
                if (!activeFlag.get()) {
                    activeFlag.set(!activeFlag.get());
                    excuteFunction(expressConfig);
                }
            }
        }
        String result = "";
        while (true){
            if (taskResultMap.containsKey(functionKey)){
                result = taskResultMap.get(functionKey);
                taskResultMap.remove(functionKey);
                break;
            } else {
                // 判断当前是否是非激活状态,非激活状态,则应该进行激活,防止存在某个突然的请求,刚好到达边界值,而不进行执行
                if (!activeFlag.get()) {
                    synchronized (activeFlag) {
                        if (!activeFlag.get()) {
                            activeFlag.set(!activeFlag.get());
                            excuteFunction(expressConfig);
                        }
                    }
                }
            }
        }
        return result;
    }

    /**
     * 执行限流方法
     */
    private void excuteFunction(Object expressConfig) {
        // 从队列中获取数据
        Object functionKey = redisTemplate.opsForList().leftPop(taskListKey);
        while (functionKey!= null ){
            if (functionMap.containsKey(functionKey.toString())) {
                // 从redis中获取当前执行状态
                //调用lua脚本并执行
                List<String> keys = new ArrayList<>();
                keys.add(this.taskExcuteFlagKey);
                DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
                redisScript.setResultType(Long.class);//返回类型是Long
                //lua文件存放在resources目录下的redis文件夹内
                redisScript.setScriptSource(new StaticScriptSource(getLuaText()));
                Object execute = redisTemplate.execute(redisScript, keys);
                // 监控是否可以执行
                while ("0".equals(execute.toString())) {
                    execute = redisTemplate.execute(redisScript, keys);
                }
                try {
                    // 拿到执行的权力,进行休眠
                    Thread.sleep(sleepTimePerTask);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                // 若为可以执行的状态,执行方法调用执行方法交给新的线程池
                scheduledExecutorService.execute(new MyExcute(functionKey.toString(),expressConfig,redisTemplate,taskExcuteFlagKey));
            } else {
                // 因为是从redis中获取的key,所以可能是其他集群的数据,因此还回去,继续下一个处理
                redisTemplate.opsForList().rightPush(taskListKey,functionKey.toString());
            }
            functionKey = redisTemplate.opsForList().leftPop(taskListKey);
        }
        activeFlag.set(!activeFlag.get());
    }

    /**
     * 组合lua脚本,执行redis操作
     * lua脚本执行redis操作时候,具有原子性,整体操作结束后,才会允许其他线程访问
     * @return
     */
    private String getLuaText(){
        // 定义可以执行标识变量
        String text = "local excuteFlag = KEYS[1];\n" +
                // 获取可执行变量的值
                "local flag = redis.call('get',excuteFlag);\n" +
                // 若执行状态变量为0,则返回0,不可执行
                "if tonumber(flag)==0 then \n" +
                " return 0;\n" +
                // 否则,执行变量值减 1 ,因为当前线程拿到了可以执行的标识符,并且要将执行标识符改为不可执行状态,因此 减 1 操作
                "else \n"+
                " redis.call('decr',excuteFlag); \n"+
                "end \n" +
                // 表示当前线程可以执行,返回 1
                "return 1";
        return text;
    }

}

class MyExcute implements Runnable{
    /**
     * 方法标识符的key
     */
    private String functionKey;

    /**
     * 方法执行依赖对象,因为我的执行方法中,需要一个对象调用,所以才传递进来
     */
    private Object expressConfig;

    /**
     * redis的操作对象
     */
    private RedisTemplate redisTemplate;

    /**
     * redis中的是否可执行状态
     */
    private String taskExcuteFlagKey;

    public MyExcute(String functionKey,Object expressConfig,RedisTemplate redisTemplate,String taskExcuteFlagKey){
        this.functionKey = functionKey;
        this.expressConfig = expressConfig;
        this.redisTemplate = redisTemplate;
        this.taskExcuteFlagKey = taskExcuteFlagKey;
    }

    @Override
    public void run() {
        ExcuteMethodParent sendMethodFather = RLimitUtils.functionMap.get(functionKey);
        RLimitUtils.functionMap.remove(functionKey);
        // 开始执行方法
        String result = excute(expressConfig,sendMethodFather);
        RLimitUtils.taskResultMap.put(functionKey,result);
    }

    /**
     * 执行方法开始
     */
    private String excute(Object expressConfig,ExcuteMethodParent father){
        // 修改状态为可以下一个执行
        redisTemplate.opsForValue().increment(taskExcuteFlagKey);
        Object excute = father.excute(expressConfig);
        return excute.toString();
    }
}

调用代码

线程类

实际测试利用线程模拟并发数量,搭建nginx模拟多个实例


import org.example.util.RLimitUtils;
import org.example.util.RedisTokenFactory;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.concurrent.TimeUnit;

public class MyThread implements Runnable{

    private RLimitUtils tokenUtils;

    public MyThread(Integer allTaskNum,Integer windowNum, TimeUnit timeUnit, RedisTemplate redisTemplate,String taskKey){
        this.tokenUtils = RedisTokenFactory.getRLimitUtils(allTaskNum,windowNum,timeUnit,redisTemplate,taskKey);
    }

    @Override
    public void run() {
        // 此处可以定义一个对象,传递到执行方法内部
        String config = "ddddddd";
        String result = tokenUtils.addFunction(config, object -> "执行时间:"+System.currentTimeMillis());
        // System.out.println(result);
    }
}

入口调用代码

@GetMapping("checkRedisLimit")
    public String checkRedisLimit(){
        // 线程池模拟并发
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(8);
        for (int i = 0; i < 100; i++) {
            scheduledExecutorService.execute(new MyThread(5,1, TimeUnit.SECONDS, redisTemplate, "redisTemplate2"));
        }
        return "asdfasdf";
    }

点击下载源码

你可能感兴趣的:(redis,redis,分布式,java)