redisson+spring aop实现限流

柔情痞子 提交于 2020-12-18 18:41:15

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>

比较简单粗暴,有问题请留言。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!