Redisson限流器

663 阅读2分钟

之前接的一个特殊需求,无法使用nginx来限流,也没办法接公司现有的限流服务,只能在业务里来做限流,对比多款限流器,最终选择了redisson

下面是代码

一、@ReqRateLimiter

自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReqRateLimiter {

    /**
     * 限流的接口
     * @return
     */
    String inter();

    /**
     * 限流速率,默认每秒100
     * @return
     */
    long rate() default 100;
  
    /**
     * 限流周期
     * @return
     */
    long rateInterval() default 1;

    /**
     * 限流速率单位
     * @return
     */
    RateIntervalUnit timeUnit() default RateIntervalUnit.SECONDS;

}

二、ReqRateLimiterAspect

切面

@Slf4j
@Aspect
@Component
public class ReqRateLimitAspect {

    @Autowired
    private Redisson redisson;

    /**
     * 根据自定义注解获取切点
     */
    @Pointcut("@annotation(reqRateLimiter)")
    public void accessLimit(ReqRateLimiter reqRateLimiter) {

    }

    @Around(value = "accessLimit(reqRateLimiter)", argNames = "pjp,reqRateLimiter")
    public Object around(ProceedingJoinPoint pjp, ReqRateLimiter reqRateLimiter) throws Throwable {
        boolean result = true;
        try {
            String imei = "";
            Object[] args = pjp.getArgs();
            ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
            MethodSignature signature = (MethodSignature) pjp.getSignature();
            Method method = signature.getMethod();
            String[] parameterNames = pnd.getParameterNames(method);
            if (parameterNames != null) {
                for (int i = 0; i < parameterNames.length; i++) {
                    if ("imei".equals(parameterNames[i])) {
                        imei = String.valueOf(args[i]);
                    }
                }
            }

            // 限流拦截器
            result = getRateLimiter(reqRateLimiter, imei);
        } catch (Exception e) {
            log.error("限流拦截器异常", e);
        }

        if (result) {
            return pjp.proceed();
        } else {
            throw new BaseException(RespCode.SYSTEM_BUSY);
        }

    }


    /**
     * 获取限流拦截器
     *
     * @param reqRateLimiter
     * @return
     */
    private Boolean getRateLimiter(ReqRateLimiter reqRateLimiter, String imei) {
        String inter = reqRateLimiter.inter();
        RRateLimiter imeiRateLimiter = redisson.getRateLimiter(inter + SysConstant.COLON + appId + SysConstant.COLON + imei);
        RRateLimiter interRateLimiter = redisson.getRateLimiter(inter);
        if (!interRateLimiter.isExists()) {
            interRateLimiter.trySetRate(RateType.PER_CLIENT, reqRateLimiter.rate(), reqRateLimiter.rateInterval(), reqRateLimiter.timeUnit());
            interRateLimiter = redisson.getRateLimiter(inter);
        }
        return interRateLimiter.tryAcquire(1);
    }


}

三、初始化限流策略


@Slf4j
@Service
public class ReqLimitServiceImpl implements ReqLimitService {

    @Autowired
    private Redisson redisson;
    @Autowired
    private OpenReqLimitService openReqLimitService;

    @Override
    public void initRule(String appId, String imei) {
        log.info("初始化限流规则,入参 appId={},imei={}", appId, imei);
        List<OpenReqLimit> limits = openReqLimitService.getLimits(appId, imei);
        for (OpenReqLimit limit : limits) {
            log.info("限流配置信息 appId={},imei={},rate={},interval={},del={}", limit.getAppId(),
                    limit.getImei(), limit.getRate(), limit.getRateInterval(), DelFlag.getByType(limit.getDelFlag()).getDesc());
            RRateLimiter rRateLimiter = redisson.getRateLimiter(limit.getInter() + SysConstant.COLON + limit.getAppId() + SysConstant.COLON + limit.getImei());
            if (rRateLimiter.isExists()) {
                //删除的配置,删除对应的限流器
                if (1 == limit.getDelFlag()) {
                    rRateLimiter.delete();
                    log.info("删除限流规则完成 appId={},imei={}", limit.getAppId(), limit.getImei());
                } else {
                    RateLimiterConfig rateLimiterConfig = rRateLimiter.getConfig();
                    // 判断配置是否更新,如果更新,重新加载限流器配置
                    if (!Objects.equals(limit.getRate(), rateLimiterConfig.getRate())
                            || !Objects.equals(RateIntervalUnit.SECONDS.toMillis(limit.getRateInterval()), rateLimiterConfig.getRateInterval())) {
                        rRateLimiter.delete();
                        rRateLimiter.trySetRate(RateType.PER_CLIENT, limit.getRate(), limit.getRateInterval(), RateIntervalUnit.SECONDS);
                        log.info("更新限流规则完成 appId={},imei={},rate={},interval={}", limit.getAppId(),
                                limit.getImei(), limit.getRate(), limit.getRateInterval());
                    }
                }
            } else if (0 == limit.getDelFlag()) {
                rRateLimiter.trySetRate(RateType.PER_CLIENT, limit.getRate(), limit.getRateInterval(),RateIntervalUnit.SECONDS);
                log.info("创建限流规则完成 appId={},imei={},rate={},interval={}", limit.getAppId(),
                        limit.getImei(), limit.getRate(), limit.getRateInterval());
            }
        }
    }


}

四、使用


@PostMapping("/location/network")
@ReqRateLimiter(inter = "/openapi/location/network")
public void network(@RequestBody @Valid LocationNetworkReq req, HttpServletResponse response) throws Exception {
    super.responseMsg(response, locationService.network(req));
}

@GetMapping("/dangerousPlace/list")
@ReqRateLimiter(inter = "/openapi/dangerousPlace/list")
public void getDangerousArea(@RequestParam("imei") String imei,
                             @RequestParam("location") String location,
                             @RequestParam(value = "radius", defaultValue = "100") Integer radius,
                             HttpServletResponse response) throws Exception {
    super.responseMsg(response, locationService.getDangerousArea(imei, location, radius));
}

五、源码分析

上面基本能实现基本的限流需求,那么redisson是怎么实现的呢?

1、trySetRate 初始化限流器

image.png

image.png

上述lua脚本,及其含义如下

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]);

脚本调用的方法参数对照表:

脚本参数名Java参数名参数值
KEYS[1]getRawName()限流器 key
ARGV[1]rate限流器令牌数
ARGV[2]unit.toMillis(rateInterval)时间间隔
ARGV[3]type.ordinal()限流器类型(全局)

2、tryAcquireAsync 尝试获取令牌

image.png

image.png 脚本调用的方法参数对照表:

脚本参数名Java参数名参数值
KEYS[1]getRawName()限流器 key
KEYS[2]getValueName()限流器令牌数量 key
KEYS[3]getClientValueName()限流器客户端令牌数量 key
KEYS[4]getPermitsName()限流器令牌 key
KEYS[5]getClientPermitsName()限流器客户端令牌 key
ARGV[1]value请求限流器令牌数
ARGV[2]System.currentTimeMillis()当前时间戳
ARGV[3]random随机数

获取令牌逻辑 download (1).png

3、availablePermits 返回可用令牌的数量

image.png

image.png

Lua 脚本调用的方法参数对照表:

脚本参数名Java参数名参数值
KEYS[1]getRawName()限流器 key
KEYS[2]getValueName()限流器令牌数量 key
KEYS[3]getClientValueName()限流器客户端令牌数量 key
KEYS[4]getPermitsName()限流器令牌 key
KEYS[5]getClientPermitsName()限流器客户端令牌 key
ARGV[1]System.currentTimeMillis()当前时间戳

4、monitor

可以进入控制台连接redis 通过monitor命令打印redis执行指令日志

image.png