redisson的限流原理
RRateLimiter limiter = redisson.getRateLimiter("myLimiter");
// one permit per 2 seconds
limiter.trySetRate(RateType.OVERALL, 1, 2, RateIntervalUnit.SECONDS);
limiter.acquire(1);
下面是RedissonRateLimiter.java#RFuture<T> tryAcquireAsync(RedisCommand<T> command, Long value)
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"local rate = redis.call('hget', KEYS[1], 'rate');"
+ "local interval = redis.call('hget', KEYS[1], 'interval');"
+ "local type = redis.call('hget', KEYS[1], 'type');"
+ "assert(rate ~= false and interval ~= false and type ~= false, 'RateLimiter is not initialized')"
+ "local valueName = KEYS[2];"
+ "if type == '1' then "
+ "valueName = KEYS[3];"
+ "end;"
+ "local currentValue = redis.call('get', valueName); "
+ "if currentValue ~= false then "
+ "if tonumber(currentValue) < tonumber(ARGV[1]) then "
+ "return redis.call('pttl', valueName); "
+ "else "
+ "redis.call('decrby', valueName, ARGV[1]); "
+ "return nil; "
+ "end; "
+ "else "
+ "redis.call('set', valueName, rate, 'px', interval); "
+ "redis.call('decrby', valueName, ARGV[1]); "
+ "return nil; "
+ "end;",
Arrays.<Object>asList(getName(), getValueName(), getClientValueName()),
value, commandExecutor.getConnectionManager().getId().toString());
evalWriteAsync(String key, Codec codec, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params);
解释:
上面是限流器代码,下面是分析
key 应该就是限流器名称
Codec 报文解码
command 根据是阻塞获取,还是非阻塞获取,还是有超时时间获取,传递的command不一样,应该是转换结果用
script lua脚本
keys
[1] 限流器名称 redis中是hashmap
[2] value 当全局控制数目用keys[2]
[3] 客户端id 当分客户端限流用 keys[3] 注意lua数组下标从1开始
脚本前3句获取限流器属性,rate interval type,也就是trySetRate设置的属性,如果全局限流,用keys[2] 否则用keys[3]
获取当前值,注意,只有一个值,控制次数
转为数字,看许可是否够,如果不够,pttl命令(当 key 不存在时,返回 -2 。 当 key 存在但没有设置剩余生存时间时,返回 -1 。 否则,以毫秒为单位,返回 key 的剩余生存时间)
许可够用则扣减
如果值不存在
set px 设置多少毫秒后过期,在trySetRate中把时间统一转为毫秒,所以这里可以直接用,见下方。
初始化之后直接扣减,然后返回nil
@Override
public RFuture<Boolean> trySetRateAsync(RateType type, long rate, long rateInterval, RateIntervalUnit unit) {
return commandExecutor.evalWriteAsync(getName(), 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.<Object>singletonList(getName()), rate, unit.toMillis(rateInterval), type.ordinal());
}
还有一个问题,集群环境下,限流器键和控制次数的键会不会不在一台机器上?
hash tag的计算规则是:取一对大括号{}之间的字符进行计算,如果key存在多对大括号,那么就取第一个左括号和第一个右括号之间的字符。如果大括号之间没有字符,则会对整个字符串进行计算。
获取名字时,使用了hash tag
public static String suffixName(String name, String suffix) {
if (name.contains("{")) {
return name + ":" + suffix;
}
return "{" + name + "}:" + suffix;
}
总结:redisson这个限流器比较简单粗暴。它利用了redis单线程执行命令和执行脚本的原子性实现了一个限流算法
别走,还没完呐...😉😉😉
AOP实现限流
注解
//List意味着可以用多个
@Target({METHOD})
@Retention(RUNTIME)
@Documented
public @interface RateLimit {
long rate();
long rateInterval();
TimeUnit unit();
String limiterName();
@Target({METHOD})
@Retention(RUNTIME)
@Documented
public @interface List {
RateLimit[] value();
}
}
切面
@Component
@Aspect
public class RateLimitAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(RateLimitAspect.class);
@Autowired
private Redisson redisson;
@Pointcut("@annotation(com.xx.yy.annotation.RateLimit.List)")
public void rateLimitList() {
}
@Pointcut("@annotation(com.xx.yy.annotation.RateLimit)")
public void rateLimit() {
}
@Before("rateLimit()")
public void acquire(JoinPoint jp) {
LOGGER.info("RateLimitAspect acquire");
acquire0(jp);
LOGGER.info("RateLimitAspect acquire exit");
}
@Before("rateLimitList()")
public void acquireList(JoinPoint jp) throws Throwable {
LOGGER.info("RateLimitAspect acquireList");
acquire0(jp);
LOGGER.info("RateLimitAspect acquireList exit");
}
private void acquire0(JoinPoint jp) {
Signature signature = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
Object[] args = jp.getArgs();
String[] parameters = methodSignature.getParameterNames();
Map<String, Object> parameterMap = new HashMap<>();
if (parameters != null) {
for (int i = 0; i < parameters.length; i++) {
parameterMap.put(parameters[i], args[i]);
}
}
LOGGER.info("acquire0 method:{} args:{} parameters:{}", targetMethod.getName(), args, Arrays.toString(parameters));
RateLimit[] rateLimits = null;
if (targetMethod.isAnnotationPresent(RateLimit.List.class)) {
RateLimit.List list = targetMethod.getAnnotation(RateLimit.List.class);
rateLimits = list.value();
} else if (targetMethod.isAnnotationPresent(RateLimit.class)) {
RateLimit limit = targetMethod.getAnnotation(RateLimit.class);
rateLimits = new RateLimit[]{limit};
}
if (rateLimits == null) {
LOGGER.warn("not config rateLimiter");
return;
}
for (RateLimit limit : rateLimits) {
String name = limit.limiterName();
if (StringUtils.isBlank(name)) {
LOGGER.error("rateLimiter name is blank,skip");
continue;
}
String realName = name;
//参数替换可优化
for (Map.Entry<String, Object> entry : parameterMap.entrySet()) {
String pattern = "#{" + entry.getKey() + "}";
if (name.contains(pattern)) {
Object val = entry.getValue();
if (val == null) {
val = "null";
}
realName = realName.replace(pattern, val.toString());
}
}
LOGGER.info("configName:{} realName:{}", name, realName);
RRateLimiter rateLimiter = redisson.getRateLimiter(realName);
rateLimiter.trySetRate(RateType.OVERALL, limit.rate(), limit.rateInterval(), limit.unit());
if (rateLimiter.tryAcquire()) {
continue;
} else {
LOGGER.warn("rateLimiter:{} acquire fail", realName);
throw new RuntimeException("调用频率超限制,请稍后再试");
}
}
}
}
使用
//多个限流,注意顺序,按用户id应该放在前面,总量限流放后面
@RateLimit.List({
@RateLimit(rate = 5, rateInterval = 1, unit = RateIntervalUnit.SECONDS, limiterName = "limit_opA_#{userId}_#{aid}"),
@RateLimit(rate = 300, rateInterval = 1, unit = RateIntervalUnit.SECONDS, limiterName = "limit_opA_#{aid}")})
public void a(Long userId, Long cid, Long aid){ }
//单个限流
@RateLimit(rate = 5, rateInterval = 1, unit = RateIntervalUnit.SECONDS, limiterName = "limit_opB_#{userId}_#{aid}")
public boolean repeatedAssist(Long userId, Long cid, Long aid){ }
redisson配置(集群模式,其他模式见官方文档:https://github.com/redisson/redisson/wiki)
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:redisson="http://redisson.org/schema/redisson"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://redisson.org/schema/redisson
http://redisson.org/schema/redisson/redisson.xsd">
<redisson:client id="redissonClient">
<!-- //scan-interval:集群状态扫描间隔时间,单位是毫秒 -->
<redisson:cluster-servers scan-interval="3000">
<redisson:node-address value="redis://${redis.pool.host1}:${redis.pool.port}"></redisson:node-address>
<redisson:node-address value="redis://${redis.pool.host2}:${redis.pool.port}"></redisson:node-address>
<redisson:node-address value="redis://${redis.pool.host3}:${redis.pool.port}"></redisson:node-address>
<redisson:node-address value="redis://${redis.pool.host4}:${redis.pool.port}"></redisson:node-address>
<redisson:node-address value="redis://${redis.pool.host5}:${redis.pool.port}"></redisson:node-address>
<redisson:node-address value="redis://${redis.pool.host6}:${redis.pool.port}"></redisson:node-address>
</redisson:cluster-servers>
</redisson:client>
</beans>
比较简单粗暴,有问题请留言。
来源:oschina
链接:https://my.oschina.net/wuxiaofei/blog/4815906