前言
接上一篇文章MK初创:Redisson分布式限流~深度解析(一)中发现的问题,本文对相关问题进行拓展改造讲解。改造功能思路:读-->抄-->测,先读懂源码关键代码及流程,再模仿源码写法抄,再根据拓展功能测试用例进行测试。
固定时间窗口算法是一种处理数据流的算法,它通过将时间分成固定大小的片段(即时间窗口),在每个窗口期间对数据进行聚合或计算。
拓展RRateLimiter实现固定时间窗口算法(按自然时间单位)
一、改造原因
当业务场景需要使用固定时间窗口算法时Redisson分布式限流没有可使用方法。
二、改造思路
- 创建RedissonRateLimiterExpand继承RRateLimiter;
- 模仿生成RRateLimiter对象方法,RedissonRateLimiterExpand中创建create静态方法用于生成RedissonRateLimiterExpand对象;
- RedissonRateLimiterExpand类中创建rateLimiterByFixedIntervalAndCalendarTime方法,实现固定时间窗口算法限流;
- 模仿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流程
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算出当前时间与下一个最近时间窗口开始时间差毫秒数 |