通过Redis限制API调用次数

转自:https://www.gaoxiaobo.com/web/service/56.html

最近在做微信公众号返利机器人,通过曲线救国的方式拿到的淘宝客工具商权限(用别人的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;
    }
}

测试截图:

在Java中发送短信,并限制每天发送次数的实现,可以通过以下几个步骤来完成: 1. **创建短信发送类**:首先,你需要创建一个短信发送类,该类负责与短信服务商的API进行交互,发送短信。 2. **使用数据库或缓存存储发送记录**:为了跟踪每天的发送次数,需要一个存储机制来记录每天的短信发送情况。可以使用数据库表来存储,或者使用缓存(如Redis)来临时存储。 3. **实现日发送次数限制逻辑**:在短信发送类中,实现一个方法来检查是否达到当天的发送限制。这个方法会首先查询存储记录,判断当天的发送次数是否已经达到限制。 4. **更新发送记录**:每次发送短信后,需要更新发送记录,增加当天的发送次数。 5. **异常处理**:如果达到发送限制,应该抛出异常或者返回特定的错误信息给调用者。 下面是一个简单的示例代码框架: ```java public class SmsService { // 假设每天限制发送10条短信 private static final int DAILY_LIMIT = 10; // 存储和检索日发送记录的方法 private static DailySmsRecordStorage recordStorage = new DailySmsRecordStorage(); public void sendSms(String phoneNumber, String message) throws Exception { // 检查是否达到每日发送次数限制 if (isOverDailyLimit(phoneNumber)) { throw new Exception("今日短信发送次数已达上限"); } // 调用短信服务商API发送短信 // ... // 更新发送记录 updateDailySmsCount(phoneNumber); } private boolean isOverDailyLimit(String phoneNumber) { // 获取该手机号当天已发送次数 int countToday = recordStorage.getSmsCountToday(phoneNumber); return countToday >= DAILY_LIMIT; } private void updateDailySmsCount(String phoneNumber) { // 增加该手机号当天的发送次数 recordStorage.incrementSmsCountToday(phoneNumber); } } class DailySmsRecordStorage { // 这里应该有实际的逻辑来存储和获取短信发送记录 // ... } ``` 在实际应用中,`DailySmsRecordStorage` 类将包含与数据库或缓存交互的逻辑,以确保发送记录的正确存储和检索。此外,为了保证系统的健壮性,你可能还需要考虑线程安全问题,特别是在高并发的情况下。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值