前言
接上一篇文章MK初创:Redisson分布式限流~深度解析(一)中发现的问题,本文对相关问题进行拓展改造讲解。改造功能思路:读–>抄–>测,先读懂源码关键代码及流程,再模仿源码写法抄,再根据拓展功能测试用例进行测试。
限流配置变更重置限流
一、改造原因
此方法无法满足当限流配置参数改动时覆盖并重置限流功能,重写此方法解决配置不同重置限流问题
rateLimiter.trySetRate(RateType.OVERALL, 4, 2, RateIntervalUnit.MINUTES)
//进入RedissonRateLimiter.java,设置限流配置参数
//lua脚本解释:设置rate、interval、type等参数时,原来存在则不设置
@Override
public RFuture<Boolean> trySetRateAsync(RateType type, long rate, long rateInterval, RateIntervalUnit unit) {
return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"redis.call('hsetnx', KEYS[1], 'rate', ARGV[1]);"
+ "redis.call('hsetnx', KEYS[1], 'interval', ARGV[2]);"
+ "return redis.call('hsetnx', KEYS[1], 'type', ARGV[3]);",
Collections.singletonList(getRawName()), rate, unit.toMillis(rateInterval), type.ordinal());
}
二、改造思路
- 创建RedissonRateLimiterExpand继承RRateLimiter;
- 模仿生成RRateLimiter对象方法,RedissonRateLimiterExpand中创建create静态方法用于生成RedissonRateLimiterExpand对象;
- 重写父类RRateLimiter中的rateLimiter.trySetRate()方法。
三、改造代码
1. RedissonRateLimiterExpand类代码
public class RedissonRateLimiterExpand extends RedissonRateLimiter {
public static RedissonRateLimiterExpand create(RedissonClient redissonClient, String name) {
Assert.notNull(redissonClient, "redissonClient不能为空!");
Assert.notBlank(name, "name不能为空!");
//创建RedissonRateLimiterExpand对象时,需要传入redissonClient获取CommandAsyncExecutor
Redisson redisson = (Redisson) redissonClient;
CommandAsyncExecutor commandExecutor = redisson.getCommandExecutor();
return new RedissonRateLimiterExpand(commandExecutor, name);
}
public RedissonRateLimiterExpand(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
}
@Override
public boolean trySetRate(RateType type, long rate, long rateInterval, RateIntervalUnit unit) {
return get(trySetRateAsync(type, rate, rateInterval, unit));
}
/**
* 滑动时间窗口算法限流模式拓展
* 设置限流规则,rate、interval、type都不为空时,判断新配置只要有一个值有变动则采用新配置,并删除旧限流数据进行重置
* @param type 全局限流/单机限流
* @param rate 速率
* @param rateInterval 限流时间间隔
* @param unit 限流时间单位
* @return
*/
@Override
public RFuture<Boolean> trySetRateAsync(RateType type, long rate, long rateInterval, RateIntervalUnit unit) {
return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
//获取限流配置中rate、interval、type参数,限流配置存于limiterKey中
"local rate = redis.call('hget', KEYS[1], 'rate');"
+ "local interval = redis.call('hget', KEYS[1], 'interval');"
+ "local type = redis.call('hget', KEYS[1], 'type');"
//其中一个为空,则默认使用原来逻辑,存在值则不替换
+ "if rate == false or interval == false or type == false then "
+ "redis.call('hsetnx', KEYS[1], 'rate', ARGV[1]);"
+ "redis.call('hsetnx', KEYS[1], 'interval', ARGV[2]);"
+ "return redis.call('hsetnx', KEYS[1], 'type', ARGV[3]);"
+ "end;"
//{limiterKey}:value作为key,存储剩余许可证数量
+ "local valueName = KEYS[2];"
//{limiterKey}:permits作为key,记录所有许可发出的时间戳及当次申请的许可证个数
+ "local permitsName = KEYS[4];"
//单机限流,则valueName、permitsName使用带有当前客户端标记作为key,如:{limiterKey}:value:b474c7d5-862c-4be2-9656-f4011c269d54
+ "if type == '1' then "
+ "valueName = KEYS[3];"
+ "permitsName = KEYS[5];"
+ "end;"
//rate、interval、type都不为空时,判断新配置只要有一个值有变动则采用新配置,并删除旧限流数据进行重置
+ "if tonumber(rate) ~= tonumber(ARGV[1]) or tonumber(interval) ~= tonumber(ARGV[2]) or tonumber(type) ~= 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], 'type', ARGV[3]);"
+ "return redis.call('del', valueName, permitsName);"
//新旧配置参数值都相等时,直接返回配置更新失败,返回0
//由于命令处理使用RedisCommands.EVAL_BOOLEAN,当返回值为1或"OK"时则为成功,其他存在的值则为失败,所以此处更新失败返回0
//可参考RedisCommands中的Convertor对象返回值的处理方式,如RedisCommands.EVAL_BOOLEAN对应的是BooleanReplayConvertor
+ "else "
+ "return 0;"
+ "end;"
,
Arrays.asList(getRawName(), getValueName(), getClientValueName(), getPermitsName(), getClientPermitsName())
, rate, unit.toMillis(rateInterval), type.ordinal());
}
//getPermitsName、getClientPermitsName、getValueName、getClientValueName
//由于父类RedissonRateLimiter中的访问权限是default,子类无访问权限,所以复制一份到子类即可
String getPermitsName() {
return suffixName(getRawName(), "permits");
}
String getClientPermitsName() {
return suffixName(getPermitsName(), commandExecutor.getConnectionManager().getId());
}
String getValueName() {
return suffixName(getRawName(), "value");
}
String getClientValueName() {
return suffixName(getValueName(), commandExecutor.getConnectionManager().getId());
}
}
2. trySetRateAsync流程
3. RedissonRateLimiterExpand限流使用
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对象
RRateLimiter rateLimiter = RedissonRateLimiterExpand.create(redissonClient, "limiterKey");
//设置限流参数
//rateType:OVERALL全局限流、PER_CLIENT当前客户端应用限流(即单机限流,自动加上当前JAVA客户端ID做标记)
//rate:限流数量(总许可证数),即时间窗口内超出此数量则限流
//rateInterval:时间窗口间隔,即多少个时间单位内作为时间窗口
//rateIntervalUnit:时间窗口时间单位
//以下举例配置说明:全局限流、2分钟时间窗口间隔最多支持4个访问数量
boolean trySetRateResult = rateLimiter.trySetRate(RateType.OVERALL, 4, 2, RateIntervalUnit.MINUTES);
//尝试申请许可证
//permits:本次申请的许可证数,不填时默认为1
boolean tryAcquireResult = rateLimiter.tryAcquire(1);
//申请成功执行业务代码
if(tryAcquireResult){
doBusiness();
}
// 关闭 RedissonClient 连接
redissonClient.shutdown();