java计算限流工具

6 篇文章 1 订阅

一、引言

        许多需求计算量都在扩大,比如合同下的门店会有三四千个,计算这些门店的数据在进行聚合,对于服务的内存和接口执行时间有着很大的影响。

        针对越来越大容量、并发高的接口或者其他计算方法,同一时间在运行的计算维度进行限制,比要计算门店设备,服务最多支持10000个门店同时在计算,相当于把资源到计算的对象维度。基于这个原因,作者编写了一个计算限流工具。

        不同于参数限制,工具针对的是服务所有线程对于该计算维度的

        限流架构如下图(计算功能如果不多可以使用本地缓存):限流。

     

二、切面工具类

1、注解

        要设置针对的方法、对象、参数和限制数量

        当限制的计算资源就在入参中,paramName就不用设置了,否则就需要在对象中取出针对的参数

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CountLimit {
    String objectName();
    String paramName();
    int limit();
    int waitTime();
}

2、切面

        切面依赖redis,将计算资源的数量进行比较看是否超过限制。

        当计算完成之后再更改redis资源,让其他线程的计算任务可以正常执行。

        当然在过程中需要保证redis的比较与资源数量设置的幂等性,采用了作者封装的另外一个工具:分布式代理锁。有兴趣的同学可以看看Redis分布式代理锁的两种实现_tingmailang的博客-CSDN博客,代理锁工具是基于Redisson通过两种方式实现代理分布式锁:

                1、ThreadLocal线程缓存 + AOP切面

                2、AOP切面 + 入参固定

        这里使用的是ThreadLocal线程缓存 + AOP切面

        针对计算资源超出限制,作者示例是做一个等待,超出一定时间再进行打断,在等待过程中尝试进入计算。

@Slf4j
@Aspect
@Component
public class CountLimitAspect {

    @Resource
    private RedissonClient redissonClient;

    @Pointcut("@annotation(com.enmonster.platform.amb.aspect.querylimit.annotation.CountLimit)")
    public void lockPointCut() {

    }

    @Around("lockPointCut() && @annotation(countLimit)")
    public Object around(ProceedingJoinPoint joinPoint, CountLimit countLimit) throws Throwable {
        LocalDateTime start = LocalDateTime.now();
        String inter = joinPoint.getTarget().getClass().getName() + joinPoint.getSignature().getName();
        String objectName = countLimit.objectName();
        String par = countLimit.paramName();
        Object[] args = joinPoint.getArgs();
        String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
        Map<String, Object> param = new HashMap<>();
        for (int i = 0; i < paramNames.length; i++) {
            param.put(paramNames[i], args[i]);
        }
        //获取限制的参数
        List<T> queryPar;
        String key = inter + objectName;
        int count;
        try {
            if (Objects.isNull(par)) {
                //如果没有设置参数,说明在入参中
                queryPar = (List<T>) param.get(objectName);
            } else {
                //说明在入参的某个对象中,有一个参数是进行限流
                Object o = param.get(objectName);
                queryPar = (List<T>) this.getFieldValueByName(par, o);
                key += par;
            }
            count = queryPar.size();
            LockUtil.set(key);
            while (!this.checkExceed(key, count, countLimit.limit())) {
                //是否超出等待时间
                if (start.plusSeconds(countLimit.waitTime()).isBefore(LocalDateTime.now())) {
                    throw new BusinessException(ErrorCodeEnum.EXCEED_LIMIT_COUNT_ERROR);
                }
                //将等待时长划分为20份
                Thread.sleep(countLimit.waitTime() * 1000 / 20);
            }
        } catch (Exception e) {
            throw new BusinessException(ErrorCodeEnum.EXCEED_LIMIT_COUNT_ERROR, e.getMessage());
        }

        try {
            return joinPoint.proceed();
        } finally {
            LockUtil.set(key);
            this.reduce(key, count);
        }
    }

    /**
     * 检查是否超出计算限制
     *
     * @param key
     * @param count
     * @param limit
     * @return
     */
    @RedisLock(key = RedisConsts.QUERY_LIMIT_LOCK, atuoRemove = true)
    public boolean checkExceed(String key, int count, int limit) {
        RBucket<Integer> bucket = redissonClient.getBucket(RedisConsts.QUERY_LIMIT_COUNT + key);
        int now = 0;
        if (bucket.isExists()) {
            now = bucket.get();
        }
        if (now + count > limit) {
            return false;
        } else {
            bucket.set(now + count);
            return true;
        }
    }


    /**
     * 减少目前在计算中的参数量级
     *
     * @param key
     * @param count
     */
    @RedisLock(key = RedisConsts.QUERY_LIMIT_LOCK, atuoRemove = true)
    public void reduce(String key, int count) {
        RBucket<Integer> bucket = redissonClient.getBucket(RedisConsts.QUERY_LIMIT_COUNT + key);
        int now = bucket.get();
        bucket.set(now - count);
    }

    private static Object getFieldValueByName(String fieldName, Object o) {
        try {
            String firstLetter = fieldName.substring(0, 1).toUpperCase();
            String getter = "get" + firstLetter + fieldName.substring(1);
            Method method = o.getClass().getMethod(getter, new Class[]{});
            Object value = method.invoke(o, new Object[]{});
            return value;
        } catch (Exception e) {
            log.error("获取属性值失败!" + e, e);
        }
        return null;
    }
}

         这是服务-计算对象维度,在实际生产环境中一般是到服务-pod-计算对象维度,修改部分实现即可。dcc.node.id是apollo配置中设置好的节点唯一标识。

@Slf4j
@Aspect
@Component
public class CountLimitAspect {

    @Resource
    private RedissonClient redissonClient;

    private static ReentrantLock lock = new ReentrantLock();

    @Value("${dcc.node.id}")
    private int workerId;

    @Pointcut("@annotation(com.enmonster.platform.amb.aspect.querylimit.annotation.CountLimit)")
    public void lockPointCut() {

    }

    @Around("lockPointCut() && @annotation(countLimit)")
    public Object around(ProceedingJoinPoint joinPoint, CountLimit countLimit) throws Throwable {
        LocalDateTime start = LocalDateTime.now();
        String inter = joinPoint.getTarget().getClass().getName() + joinPoint.getSignature().getName();
        String objectName = countLimit.objectName();
        String par = countLimit.paramName();
        Object[] args = joinPoint.getArgs();
        String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
        Map<String, Object> param = new HashMap<>();
        for (int i = 0; i < paramNames.length; i++) {
            param.put(paramNames[i], args[i]);
        }
        //获取限制的参数
        List<T> queryPar;
        String key = inter + objectName;
        int count;
        try {
            if (Objects.isNull(par)) {
                //如果没有设置参数,说明在入参对象中
                queryPar = (List<T>) param.get(objectName);
            } else {
                //说明在入参的某个对象中,有一个参数是进行限流
                Object o = param.get(objectName);
                queryPar = (List<T>) this.getFieldValueByName(par, o);
                key += par;
            }
            count = queryPar.size();
            while (!this.checkExceed(key, count, countLimit.limit())) {
                //是否超出等待时间
                if (start.plusSeconds(countLimit.waitTime()).isBefore(LocalDateTime.now())) {
                    throw new BusinessException(ErrorCodeEnum.EXCEED_LIMIT_COUNT_ERROR);
                }
                //将等待时长划分为20份,设置根据服务情况调整,也可以做成配置
                Thread.sleep(countLimit.waitTime() * 1000 / 20);
            }
        } catch (Exception e) {
            throw new BusinessException(ErrorCodeEnum.EXCEED_LIMIT_COUNT_ERROR, e.getMessage());
        }

        try {
            return joinPoint.proceed();
        } finally {
            this.reduce(key, count);
        }
    }

    /**
     * 检查是否超出计算限制
     *
     * @param key
     * @param count
     * @param limit
     * @return
     */
//    @RedisLock(key = RedisConsts.COUNT_LIMIT_LOCK, atuoRemove = true)
    public boolean checkExceed(String key, int count, int limit) {
        try {
            if (lock.tryLock()) {
                RBucket<Integer> bucket = redissonClient.getBucket(RedisConsts.COUNT_LIMIT_COUNT + workerId + key);
                int now = 0;
                if (bucket.isExists()) {
                    now = bucket.get();
                }
                if (now + count > limit) {
                    return false;
                } else {
                    bucket.set(now + count);
                    return true;
                }
            } else {
                return false;
            }
        } finally {
            lock.unlock();
        }
    }


    /**
     * 减少目前在查询中的参数量级
     *
     * @param key
     * @param count
     */
//    @RedisLock(key = RedisConsts.COUNT_LIMIT_LOCK, atuoRemove = true)
    public void reduce(String key, int count) {
        try {
            if (lock.tryLock(10, TimeUnit.SECONDS)) {
                RBucket<Integer> bucket = redissonClient.getBucket(RedisConsts.COUNT_LIMIT_COUNT + workerId + key);
                int now = bucket.get();
                bucket.set(now - count);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    private static Object getFieldValueByName(String fieldName, Object o) {
        try {
            String firstLetter = fieldName.substring(0, 1).toUpperCase();
            String getter = "get" + firstLetter + fieldName.substring(1);
            Method method = o.getClass().getMethod(getter, new Class[]{});
            Object value = method.invoke(o, new Object[]{});
            return value;
        } catch (Exception e) {
            log.error("获取属性值失败!" + e, e);
            throw new BusinessException(ErrorCodeEnum.EXCEED_LIMIT_COUNT_ERROR, e.getMessage());
        }
    }
}

         一般服务大容量的计算功能不会太多,可以进一步优化使用本地缓存

@Slf4j
@Aspect
@Component
public class CountLimitAspect<T> {

    private static ReentrantLock lock = new ReentrantLock();

    private static volatile ConcurrentHashMap<String, Integer> countMap = new ConcurrentHashMap<>();

    @Value("${dcc.node.id}")
    private int workerId;

    @Pointcut("@annotation(com.enmonster.platform.amb.aspect.countlimit.annotation.CountLimit)")
    public void lockPointCut() {

    }

    @Around("lockPointCut() && @annotation(countLimit)")
    public Object around(ProceedingJoinPoint joinPoint, CountLimit countLimit) throws Throwable {
        LocalDateTime start = LocalDateTime.now();
        String inter = joinPoint.getTarget().getClass().getName() + joinPoint.getSignature().getName();
        String objectName = countLimit.objectName();
        String par = countLimit.paramName();
        Object[] args = joinPoint.getArgs();
        String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
        Map<String, Object> param = new HashMap<>();
        for (int i = 0; i < paramNames.length; i++) {
            param.put(paramNames[i], args[i]);
        }

        //获取限制的参数
        List<T> queryPar;
        String key = inter + objectName;
        int count;
        try {
            if (StringUtils.isBlank(par)) {
                //如果没有设置参数,说明在入参对象中
                queryPar = (List<T>) param.get(objectName);
            } else {
                //说明在入参的某个对象中,有一个参数是进行限流
                Object o = param.get(objectName);
                queryPar = (List<T>) this.getFieldValueByName(par, o);
                key += par;
            }
            count = queryPar.size();
            while (!this.checkExceed(key, count, countLimit.limit())) {
                //是否超出等待时间
                if (start.plusSeconds(countLimit.waitTime()).isBefore(LocalDateTime.now())) {
                    throw new BusinessException(ErrorCodeEnum.EXCEED_LIMIT_COUNT_ERROR);
                }
                //将等待时长划分为20份
                Thread.sleep(countLimit.waitTime() * 1000 / 20);
            }
        } catch (Exception e) {
            throw new BusinessException(ErrorCodeEnum.EXCEED_LIMIT_COUNT_ERROR, e.getMessage());
        }

        try {
            return joinPoint.proceed();
        } finally {
            this.reduce(key, count);
        }
    }

    /**
     * 检查是否超出计算限制
     *
     * @param key
     * @param count
     * @param limit
     * @return
     */
    public boolean checkExceed(String key, int count, int limit) {
        try {
            if (lock.tryLock()) {
                int now = countMap.getOrDefault(workerId + key, 0);
                if (now + count > limit) {
                    return false;
                } else {
                    countMap.put(workerId + key, now + count);
                    return true;
                }
            } else {
                return false;
            }
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }


    /**
     * 减少目前在查询中的参数量级
     *
     * @param key
     * @param count
     */
    public void reduce(String key, int count) {
        try {
            if (lock.tryLock(10, TimeUnit.SECONDS)) {
                int now = countMap.get(workerId + key);
                countMap.put(workerId + key, now - count);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private static Object getFieldValueByName(String fieldName, Object o) {
        try {
            String firstLetter = fieldName.substring(0, 1).toUpperCase();
            String getter = "get" + firstLetter + fieldName.substring(1);
            Method method = o.getClass().getMethod(getter, new Class[]{});
            Object value = method.invoke(o, new Object[]{});
            return value;
        } catch (Exception e) {
            log.error("获取属性值失败!" + e, e);
            throw new BusinessException(ErrorCodeEnum.EXCEED_LIMIT_COUNT_ERROR, e.getMessage());
        }
    }
}

三、使用

        这里作者使用一个接口做示例,其实任何一个计算方法都可以使用,加@CountLimit注解,把限流参数填一下就可以。

    @ApiOperation("测试计算限流")
    @ApiResponses(@ApiResponse(code = HttpStatus.SC_OK, message = "测试计算限流"))
    @PostMapping("/count-limit")
    @MethodLogger
    @CountLimit(objectName = "request", paramName = "shopIdList", limit = 10000, waitTime = 2)
    public BaseResponse countLimit(@RequestBody CheckDeviceBaseRequestDTO request) {
        return BaseResponse.createSuccessResult(offLineReportFacade.closeBill(request));
    }

四、总结

        目前对于计算资源的需求越来越大,很多需求上线之前根本没法估算会遇到多大的qps,也就没法知道到底有多少资源同时在计算,有兴趣的同学可以试试作者的计算限流工具,至少不要让服务崩掉。

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胖当当技术

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值