您现在的位置是:首页 > 技术人生 > 后端技术后端技术

通过Redis限制API调用次数

高晓波2020-02-18【后端技术】人已围观

简介最近在做微信公众号返利机器人,通过曲线救国的方式拿到的淘宝客工具商权限(用别人的App Key),在服务器搭建了透传程序。
使用过程中发现一个问题:阿里妈妈对淘宝API的调用频率有限制,报错信息反馈是40次/秒。

最近在做微信公众号返利机器人,通过曲线救国的方式拿到的淘宝客工具商权限(用别人的App Key),在服务器搭建了透传程序。

使用过程中发现一个问题:

 
{
	"error_response": {
		"msg": "Remote service error",
		"code": 15,
		"sub_msg": "您的账号因调用不符合要求,当前触发限流。为保障广大推广者正常使用系统,会有账号维度的限流机制:通过API、淘宝客PC后台、联盟APP或使用第三方工具获取数据,同账号会统一限流。同一个账号1秒总请求次数约200次,其中调用API约40次,超过或接近临界值,会触发限流。建议您逐一排查各个渠道的总调用量,降低调用频次。此外,在淘宝客PC后台_效果报表_推广者推广明细页面,注意事项第3条会有订单查询方式建议,违反建议也会触发限流。请按建议调整请求策略。",
		"sub_code": "9100",
		"request_id": "xxxxx"
	}
}


阿里妈妈对淘宝客API的调用频率有限制,报错信息反馈是40次/秒。

那么需要对透传程序进行限流操作,因为以后可能还要开放给朋友们一起使用,好东西大家一起分享嘛。

进入正题,查阅相关资料,通过redis的INCR操作可实现所需功能。


什么是INCR操作?

INCR key

将 key 中储存的数字值增一。

如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。

如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。

本操作的值限制在 64 位(bit)有符号数字表示之内。


思路:当API被调用时,在调用淘宝API前进行INCR key,key可以是ip地址相关,用户相关,或是全局的一个key。如果返回值为1,则表示刚开始调用,赋予key过期时间,然后判断返回值是否大于设定的limit,如果大于抛异常,伪代码如下:
 

let limit = 100;
let limit_seconds_time = 60
let count = redis.incr(key);

if count == 1
redis.expire(key, 60)

if count > limit
 throw exception;

service()



实现代码如下:
 

APILimit注解:

package com.gaoxiaobo.common.annotations;

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Order(Ordered.HIGHEST_PRECEDENCE)
public @interface APILimit {
    /**
     *
     * @Description: 限制某时间段内可以访问的次数,默认设置100
     * @return
     * 
     */
    int limitCounts() default 100;

    /**
     *
     * @Description: 限制访问的某一个时间段,单位为秒,默认值1分钟即可
     * @return
     * 
     */
    int timeSecond() default 60;
}


限流Aspect
package com.gaoxiaobo.common.aspect;

import com.gaoxiaobo.common.annotations.APILimit;
import com.gaoxiaobo.common.exception.BusiException;
import com.gaoxiaobo.service.modules.RedisService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

import static com.gaoxiaobo.common.constants.ApiConstants.*;

@Aspect
@Component
public class APILimitAspect {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private RedisService redisService;

    @Pointcut("execution(* com.gaoxiaobo.controller.modules.*.*(..))")
    public void before(){
    }

    @Before("before()")
    public void requestLimit(JoinPoint joinPoint) throws Exception {
        // 获取HttpRequest
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 判断request不能为空
        if (request == null) {
            throw new BusiException("HttpServletRequest有误...");
        }

        APILimit limit = this.getAnnotation(joinPoint);

        //controller 方法限流
        if(limit != null) {
            String ip = request.getRemoteAddr();
            String uri = request.getRequestURI();
            logger.debug("ip:{}, uri:{}", ip, uri);

            String redisKey = METHOD_LIMIT_KEY_PRE + uri + ":" + ip;
            long methodCounts = redisService.incr(redisKey);

            // 如果该key不存在,则从0开始计算,并且当count为1的时候,设置过期时间
            if (methodCounts == 1) {
                redisService.expire(redisKey, limit.timeSecond());
            }

            // 如果redis中的count大于限制的次数,则报错
            if (methodCounts > limit.limitCounts()) {
                throw new BusiException("api调用超限");
            }
        }


        //全局限流,每秒30次
        long count = redisService.incr(GLOBAL_LIMIT_KEY);
        if (count == 1) {
            redisService.expire(GLOBAL_LIMIT_KEY, 1);
        }

        if (count > GLOBAL_LIMIT) {
            throw new BusiException("api调用超限");
        }
    }

    /**
     *
     * @Description: 获得注解
     * @param joinPoint
     * @return
     * @throws Exception
     */
    private APILimit getAnnotation(JoinPoint joinPoint) throws Exception {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        if (method != null) {
            return method.getAnnotation(APILimit.class);
        }
        return null;
    }
}


测试截图:

Tags:淘宝客   redis

很赞哦! ()

文章评论