redisson+spring aop實現限流

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"/>

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章