MK初创:Redisson分布式限流~固定时间窗口算法拓展(三)

242 阅读4分钟

前言

接上一篇文章MK初创:Redisson分布式限流~深度解析(一)中发现的问题,本文对相关问题进行拓展改造讲解。改造功能思路:读-->抄-->测,先读懂源码关键代码及流程,再模仿源码写法抄,再根据拓展功能测试用例进行测试。

固定时间窗口算法是一种处理数据流的算法,它通过将时间分成固定大小的片段(即时间窗口),在每个窗口期间对数据进行聚合或计算。

拓展RRateLimiter实现固定时间窗口算法(按自然时间单位)

一、改造原因

当业务场景需要使用固定时间窗口算法时Redisson分布式限流没有可使用方法。

二、改造思路

  1. 创建RedissonRateLimiterExpand继承RRateLimiter;
  2. 模仿生成RRateLimiter对象方法,RedissonRateLimiterExpand中创建create静态方法用于生成RedissonRateLimiterExpand对象;
  3. RedissonRateLimiterExpand类中创建rateLimiterByFixedIntervalAndCalendarTime方法,实现固定时间窗口算法限流;
  4. 模仿RateIntervalUnit自定义RateFixedIntervalUnit固定时间窗口单位枚举类,RateFixedIntervalUnit中根据时间单位拓展获取“获取当前时间跟自然时间窗口下一个时间开始时间点时间戳差值,作为时间窗口key超时时间。

三、改造代码

1. RateFixedIntervalUnit类代码

/**
 * @Description:固定时间窗口单位
 */
public enum RateFixedIntervalUnit {

    MILLISECONDS {
        @Override
        public long getExpireMillis(long rateInterval) {
            //毫秒直接返回毫秒间隔
            return rateInterval;
        }
    },

    SECONDS {
        @Override
        public long getExpireMillis(long rateInterval) {
            return this.getExpireMillisByIntervalUnit(now -> {
                DateTime nextDate = DateUtil.offsetSecond(now, (int) rateInterval);
                return DateUtil.beginOfSecond(nextDate);
            });
        }
    },

    MINUTES {
        @Override
        public long getExpireMillis(long rateInterval) {
            return this.getExpireMillisByIntervalUnit(now -> {
                DateTime nextDate = DateUtil.offsetMinute(now, (int) rateInterval);
                return DateUtil.beginOfMinute(nextDate);
            });
        }
    },

    HOURS {
        @Override
        public long getExpireMillis(long rateInterval) {
            return this.getExpireMillisByIntervalUnit(now -> {
                DateTime nextDate = DateUtil.offsetHour(now, (int) rateInterval);
                return DateUtil.beginOfHour(nextDate);
            });
        }
    },

    DAYS {
        @Override
        public long getExpireMillis(long rateInterval) {
            return this.getExpireMillisByIntervalUnit(now -> {
                DateTime nextDate = DateUtil.offsetDay(now, (int) rateInterval);
                return DateUtil.beginOfDay(nextDate);
            });
        }
    };

    /**
     * 根据窗口间隔获取当前固定自然时间窗口过期时间
     *
     * @param rateInterval
     * @return
     */
    public abstract long getExpireMillis(long rateInterval);

    /**
     * 获取当前时间跟自然时间窗口下一个时间开始时间点时间戳差值,作为时间窗口key超时时间
     *
     * @param function
     * @return
     */
    public long getExpireMillisByIntervalUnit(Function<Date, Date> function) {
        // 获取当前时间
        Date now = DateUtil.date();
        Assert.notNull(function, "RateFixedIntervalUnit.getExpireMillisByIntervalUnit:function can’t be null");
        // 获取自然时间窗口下一个时间开始时间点
        Date nextDateBegin = function.apply(now);
        // 计算两个时间的时间差
        long between = DateUtil.between(now, nextDateBegin, DateUnit.MS);
        return between;
    }
}

2. RedissonRateLimiterExpand类代码

/**
 * @Description:redisson限流固定时间窗口算法扩展
 */
public class RedissonRateLimiterExpand extends RedissonRateLimiter {

    public static RedissonRateLimiterExpand create(RedissonClient redissonClient, String name) {
        Assert.notNull(redissonClient, "redissonClient不能为空!");
        Assert.notBlank(name, "name不能为空!");

        Redisson redisson = (Redisson) redissonClient;
        CommandAsyncExecutor commandExecutor = redisson.getCommandExecutor();
        return new RedissonRateLimiterExpand(commandExecutor, name);
    }

    public RedissonRateLimiterExpand(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
    }

    public boolean rateLimiterByFixedIntervalAndCalendarTime(long rate, long rateInterval, RateFixedIntervalUnit rateFixedIntervalUnit) {
        return get(rateLimiterByFixedIntervalAndCalendarTimeAsync(rate, rateInterval, rateFixedIntervalUnit));
    }

    /**
     * redis实现固定时间窗口算法限流,实现自然时间单位限流,如:自然日、自然时、自然分、自然秒,自然毫秒
     * 自然时间即只限流当前天,当前小时,当前分钟,当前秒,当前毫秒
     * rate、interval、intervalUnit其中一个配置变更时,则清除旧限流数据,并重置限流配置
     *
     * @param rate                  限流数量
     * @param rateInterval          限流时间间隔
     * @param rateFixedIntervalUnit 限流时间单位
     * @return
     */
    public RFuture<Boolean> rateLimiterByFixedIntervalAndCalendarTimeAsync(long rate, long rateInterval, RateFixedIntervalUnit rateFixedIntervalUnit) {

        Assert.isTrue(rate > 0 && rateInterval > 0, "限流数量、限流时间间隔均不能小于0!");
        Assert.notNull(rateFixedIntervalUnit, "固定时间窗口单位不能为空!");

        return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_NULL_BOOLEAN,
                "local rate = redis.call('hget', KEYS[1], 'rate');"
                        + "local interval = redis.call('hget', KEYS[1], 'interval');"
                        + "local intervalUnit = redis.call('hget', KEYS[1], 'interval_unit');"
                        //其中一个为空,则默认使用原来逻辑,存在值则不替换
                        + "if rate == false or interval == false or intervalUnit == false then "
                            + "redis.call('hsetnx', KEYS[1], 'rate', ARGV[1]);"
                            + "redis.call('hsetnx', KEYS[1], 'interval', ARGV[2]);"
                            + "redis.call('hsetnx', KEYS[1], 'interval_unit', ARGV[3]);"
                        + "end;"

                        + "local valueName = KEYS[2];"
                        //rate、interval、intervalUnit都不为空时,判断新配置只要有一个值有变动则采用新配置,并删除旧限流数据进行重置
                        + "if tonumber(rate) ~= tonumber(ARGV[1]) or tonumber(interval) ~= tonumber(ARGV[2]) or tonumber(intervalUnit) ~= tonumber(ARGV[3]) then "
                            + "redis.call('hset', KEYS[1], 'rate', ARGV[1]);"
                            + "redis.call('hset', KEYS[1], 'interval', ARGV[2]);"
                            + "redis.call('hset', KEYS[1], 'interval_unit', ARGV[3]);"
                            + "redis.call('del', valueName);"
                        + "end;"

                        +"rate = redis.call('hget', KEYS[1], 'rate');" +
                        "local expireMillis = tonumber(ARGV[4]);" +
                        "local currentCount = redis.call('get', valueName);" +
                        //当前时间窗口已统计次数小于限流数量时,可放行
                        "if currentCount ~= false then " +
                            "if tonumber(currentCount) < tonumber(rate) then " +
                                "redis.call('INCR', valueName);" +
                                "return nil;" +
                            "else " +
                                "return 1;" +
                            "end;" +
                        "else " +
                            "redis.call('INCR', valueName);" +
                            "redis.call('PEXPIRE', valueName ,expireMillis);" +
                            "return nil;" +
                        "end;"
                ,
                Arrays.asList(getRawName(), getValueName()),
                rate,rateInterval,rateFixedIntervalUnit.name(), rateFixedIntervalUnit.getExpireMillis(rateInterval));
    }
}

3. rateLimiterByFixedIntervalAndCalendarTimeAsync流程

Redisson原理限流流程图-rateLimiterByFixedIntervalAndCalendarTimeAsync.png

4. rateLimiterByFixedIntervalAndCalendarTime限流使用

Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);
//限流key
String limiterKey = "myRateLimiter";
//根据redissonClient获取RRateLimiter,并设置限流key,注意此处用的是RedissonRateLimiterExpand对象
RedissonRateLimiterExpand rateLimiter = RedissonRateLimiterExpand.create(redissonClient, "limiterKey");
//设置限流参数

//rate:限流数量(总许可证数),即时间窗口内超出此数量则限流
//rateInterval:时间窗口间隔,即多少个时间单位内作为时间窗口
//RateFixedIntervalUnit:时间窗口时间单位   
//以下举例配置说明:2分钟时间窗口间隔最多支持5个访问数量
//尝试申请许可证
boolean tryAcquireResult = 
rateLimiter.rateLimiterByFixedIntervalAndCalendarTime(5, 2, RateFixedIntervalUnit.MINUTES);
//申请成功执行业务代码
if(tryAcquireResult){
    doBusiness();
}
// 关闭 RedissonClient 连接
redissonClient.shutdown();

5. 相关Key分析

Key说明保存相关参数
limiterKey存储当前限流配置,永久保存rate:总许可证数;interval:时间间隔数;interval_unit:时间单位字符串
{limiterKey}:value存储剩余许可证数量,设置超时超时时间:根据rate、interval_unit算出当前时间与下一个最近时间窗口开始时间差毫秒数

6. 时间窗口演示

image.png

image.png