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();
int expireSecond() default 60;
@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());
rateLimiter.expire(limit.expireSecond(),TimeUnit.SECONDS);//限流器過期自動刪除
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>
比較簡單粗暴,有問題請留言。 追加:redisson 3.14.0的限流方式更新了:
private <T> 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];"
+ "local permitsName = KEYS[4];"
+ "if type == '1' then "
+ "valueName = KEYS[3];"
+ "permitsName = KEYS[5];"
+ "end;" // 獲取配置,如果分客戶端限流,獲取對應的key
+ "assert(tonumber(rate) >= tonumber(ARGV[1]), 'Requested permits amount could not exceed defined rate'); "
+ "local currentValue = redis.call('get', valueName); "
+ "if currentValue ~= false then " // 如果限流key存在
+ "local expiredValues = redis.call('zrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval); "
+ "local released = 0; "
+ "for i, v in ipairs(expiredValues) do "
+ "local random, permits = struct.unpack('fI', v);"
+ "released = released + permits;"
+ "end; "//獲取已經過期的,把過期的許可收回,zrange獲取的是0到(當前時間點減去一個週期)之間的
+ "if released > 0 then "
+ "redis.call('zrem', permitsName, unpack(expiredValues)); "
+ "currentValue = tonumber(currentValue) + released; "
+ "redis.call('set', valueName, currentValue);"
+ "end;"//刪除這些過期的,收回許可
//如果許可不夠,返回最近即將失效的那個,半括號代表開區間
+ "if tonumber(currentValue) < tonumber(ARGV[1]) then "
+ "local nearest = redis.call('zrangebyscore', permitsName, '(' .. (tonumber(ARGV[2]) - interval), tonumber(ARGV[2]), 'withscores', 'limit', 0, 1); "
+ "local random, permits = struct.unpack('fI', nearest[1]);" //這步是不是沒必要
+ "return tonumber(nearest[2]) - (tonumber(ARGV[2]) - interval);"//返回到過期時候的時間
+ "else "//如果許可夠用,減掉此次發放的許可,記錄時間點 fI指的是float和integer
+ "redis.call('zadd', permitsName, ARGV[2], struct.pack('fI', ARGV[3], ARGV[1])); "
+ "redis.call('decrby', valueName, ARGV[1]); "
+ "return nil; "
+ "end; "
+ "else " //如果不存在,設置值,記錄分發許可的信息,使用zset,排序值是毫秒數
+ "redis.call('set', valueName, rate); "
+ "redis.call('zadd', permitsName, ARGV[2], struct.pack('fI', ARGV[3], ARGV[1])); "
+ "redis.call('decrby', valueName, ARGV[1]); "
+ "return nil; "
+ "end;",
Arrays.asList(getName(), getValueName(), getClientValueName(), getPermitsName(), getClientPermitsName()),
value, System.currentTimeMillis(), ThreadLocalRandom.current().nextLong());
}
好像變得複雜了,下面簡單分析一下:
KEYS:[限流器配置key,記錄值的key,分客戶端限流記錄值的key,分發許可的zset key,分客戶端分發許可的zset key]
ARGV:[需要幾個許可,系統當前毫秒數,隨機值]
對比上下兩個實現,有什麼區別?
拿1秒內5個許可來舉例:
3.14.0之前的:
3.14.0:
追加:
在springboot中使用正常,在spring項目裏,發現切面中獲取方法參數methodSignature.getParameterNames爲null,targetMethod.isAnnotationPresent也返回false,原因是代理類的實現方式不同引起的,需要在xml中添加 <aop:aspectj-autoproxy proxy-target-class="true"/>