记录一次在公司遇到的错误:当时对接了高德的周边搜索接口开放给前端调用,但是只是过了3个小时前端就过来找我说接口报错了,我看日志发现问题是高德接口调用次数在当日已经超过了5000次所以高德做限制了,于是决定做接口我调用频率限制的代码。
限制接口调用频率的方法一般都是通过时间来判断,所以最好的办法就是使用redis来记录接口调用的时间。
这里我先放上使用 HashMap<String, Long> 代替redis的一种非常简单的写法。
- import java.time.LocalDateTime;
- import java.util.HashMap;
-
- /**
- * @author QingXun123
- * @version 1.0.0
- * @since 2023-08-29
- */
- public class SubmitBufferSingleton {
-
- private static HashMap<String, Long> hashMap = new HashMap<>();
-
- private SubmitBufferSingleton() {
- }
-
- public static HashMap<String, Long> getInstance() {
- return hashMap;
- }
- }
SubmitBufferSingleton 用来获取唯一的 HashMap<String, Long>,其中的Value是从 1970-01-01T00:00:00Z(协调世界时,UTC)到当前时间点之间的毫秒数。
- import org.springframework.core.Ordered;
- import org.springframework.core.annotation.Order;
-
- import java.lang.annotation.*;
-
- /**
- * @author QingXun123
- * @version 1.0.0
- * @since 2023-08-29
- */
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.TYPE, ElementType.METHOD})
- @Documented
- @Order(Ordered.HIGHEST_PRECEDENCE)
- public @interface RequestLimit {
-
- long time() default 5000;
- }
time是调用接口的间隔时间,默认是 5000 毫秒。
- import com.qingxun.aspect.annotation.RequestLimit;
- import com.qingxun.singleton.SubmitBufferSingleton;
- import lombok.extern.slf4j.Slf4j;
- import org.aspectj.lang.ProceedingJoinPoint;
- import org.aspectj.lang.annotation.Around;
- import org.aspectj.lang.annotation.Aspect;
- 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.time.Instant;
- import java.util.HashMap;
-
- /**
- * @author QingXun123
- * @version 1.0.0
- * @since 2023-08-29
- */
- @Aspect
- @Component
- @Slf4j
- public class NoRepeatSubmitAop {
-
- @Synchronized // 作用是创建一个互斥锁,保证只有一个线程对 SubmitBufferSingleton.getInstance() 这个变量进行修改。
- @Around("execution(* com.example..client.amap.controller..*.*(..)) && @annotation(nrs)") // 这里要根据自己项目中的controller包路径来配置好
- public Object arround(ProceedingJoinPoint pjp, RequestLimit nrs) throws Throwable {
-
- ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
- HttpServletRequest request = attributes.getRequest();
- Object[] args = pjp.getArgs();
- String key = getIp(request) + " :" + request.getServletPath();
- Long time = nrs.time();
- Object o = null;
- HashMap<String, Long> hashMap = SubmitBufferSingleton.getInstance();
- long nowTime = Instant.now().toEpochMilli();
- if (!hashMap.containsKey(key)) {
- hashMap.put(key, nowTime + time);
- o = pjp.proceed();
- return o;
- } else {
- if (nowTime > hashMap.get(key)) {
- hashMap.put(key, nowTime + time);
- o = pjp.proceed();
- return o;
- } else {
- log.error("操作过于频繁 {}", key);
- return "操作过于频繁";
- }
- }
-
- }
-
-
- // 获取调用者ip
- private static String getIp(HttpServletRequest request){
- String ip = request.getHeader("x-forwarded-for");
- if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
- ip = request.getHeader("Proxy-Client-IP");
- }
- if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
- ip = request.getHeader("WL-Proxy-Client-IP");
- }
- if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
- ip = request.getHeader("HTTP_CLIENT_IP");
- }
- if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
- ip = request.getHeader("HTTP_X_FORWARDED_FOR");
- }
- if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
- ip = request.getRemoteAddr();
- }
- return ip;
- }
-
- }
这里其实可以直接将 HashMap<String, Long> 放到 NoRepeatSubmitAop 中的。将这个分离出来是为了解耦。
- import com.qingxun.singleton.SubmitBufferSingleton;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.scheduling.annotation.Scheduled;
- import org.springframework.stereotype.Component;
-
- import java.time.Instant;
- import java.time.LocalDateTime;
- import java.util.HashMap;
- import java.util.Iterator;
- import java.util.Map;
-
- /**
- * @author QingXun123
- * @version 1.0.0
- * @since 2023-08-29
- */
- @Component
- @Slf4j
- public class NoRepeatSubmitTask {
-
- @Scheduled(cron = "0 0 1 * * ?")
- public void start() {
- HashMap<String, Long> hashMap = SubmitBufferSingleton.getInstance();
- Iterator<Map.Entry<String, Long>> iterator = hashMap.entrySet().iterator();
- while (iterator.hasNext()) {
- Map.Entry<String, Long> next = iterator.next();
- String key = next.getKey();
- Long value = next.getValue();
- if (value > Instant.now().toEpochMilli()) {
- hashMap.remove(key);
- }
- }
- // 如果对时间没有特别严格的要求就直接clear
- // hashMap.clear();
- }
- }
这里设置一个定时器在每天的凌晨1点清理一下HashMap数据,防止 HashMap<String, Long> 越来越大。将 HashMap<String, Long> 从 NoRepeatSubmitAop 中抽出来的优点在这里也体现出来了。
如果使用了redis就不需要用到定时器,直接设置好过期时间让redis自己删除就可以了。
- import com.qingxun.aspect.annotation.RequestLimit;
- import lombok.extern.slf4j.Slf4j;
- import org.aspectj.lang.ProceedingJoinPoint;
- import org.aspectj.lang.annotation.Around;
- import org.aspectj.lang.annotation.Aspect;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.stereotype.Component;
- import org.springframework.web.context.request.RequestContextHolder;
- import org.springframework.web.context.request.ServletRequestAttributes;
-
- import javax.annotation.Resource;
- import javax.servlet.http.HttpServletRequest;
- import java.util.concurrent.TimeUnit;
-
- /**
- * @author QingXun123
- * @version 1.0.0
- * @since 2023-08-29
- */
- @Aspect
- @Component
- @Slf4j
- public class NoRepeatSubmitAop {
-
- @Resource
- public RedisTemplate redisTemplate;
-
- //@Synchronized // 根据情况使用
- @Around("execution(* com.example.controller..*.*(..)) && @annotation(nrs)") // 这里要根据自己项目中的controller包路径来配置好
- public Object arround(ProceedingJoinPoint pjp, RequestLimit nrs) throws Throwable {
-
- ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
- HttpServletRequest request = attributes.getRequest();
- Object[] args = pjp.getArgs();
- String key = getIp(request) + " :" + request.getServletPath();
- Object o = null;
- Long time = nrs.time();
-
- if (!redisTemplate.hasKey(key)) {
- redisTemplate.opsForValue().set(key, 0, time.intValue(), TimeUnit.MILLISECONDS);
- o = pjp.proceed();
- return o;
- } else {
- log.error("操作过于频繁 {}", key);
- return "操作过于频繁";
- }
-
- }
-
- // 获取调用者ip
- private static String getIp(HttpServletRequest request){
- String ip = request.getHeader("x-forwarded-for");
- if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
- ip = request.getHeader("Proxy-Client-IP");
- }
- if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
- ip = request.getHeader("WL-Proxy-Client-IP");
- }
- if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
- ip = request.getHeader("HTTP_CLIENT_IP");
- }
- if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
- ip = request.getHeader("HTTP_X_FORWARDED_FOR");
- }
- if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
- ip = request.getRemoteAddr();
- }
- return ip;
- }
-
- }
这里直接判断redis中是否有这个key,没有就说明没有调用过这个接口或者已经超过了限制时间,所以后端就可以返回数据给前端。
测试一下是否有用。

这里我们简单写了一个测试接口,然后在 swagger 中调用成功。

再点一下就失败了。


测试通过。
事实证明后端还是不能相信前端,前端调试3个小时调了我5000次的接口😂。