Przeglądaj źródła

1、增加内存限流方式
2、增加分布式限流方式

dsx 2 lat temu
rodzic
commit
86850a292c

+ 31 - 0
pom.xml

@@ -36,6 +36,32 @@
             <groupId>com.fdkankan</groupId>
             <artifactId>4dkankan-common-web</artifactId>
             <version>3.0.0-SNAPSHOT</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.bytedeco</groupId>
+                    <artifactId>javacv</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.bytedeco</groupId>
+                    <artifactId>javacpp</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.bytedeco</groupId>
+                    <artifactId>javacv-platform</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>joinery</groupId>
+                    <artifactId>jave</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.aliyun</groupId>
+                    <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.aliyun</groupId>
+                    <artifactId>aliyun-java-sdk-core</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
 
         <dependency>
@@ -175,6 +201,11 @@
             <version>1.5.24</version>
         </dependency>
 
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>30.1-jre</version>
+        </dependency>
 
     </dependencies>
     <dependencyManagement>

+ 23 - 0
src/main/java/com/fdkankan/openApi/component/Limit.java

@@ -0,0 +1,23 @@
+package com.fdkankan.openApi.component;
+
+import java.lang.annotation.*;
+import java.util.concurrent.TimeUnit;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD})
+@Documented
+public @interface Limit {
+
+    // 最多访问次数
+    double permitsPerSecond();
+
+    // 时间
+    long timeout();
+    
+    // 时间类型
+    TimeUnit timeunit() default TimeUnit.MILLISECONDS;
+
+    // 提示信息
+    String msg() default "系统繁忙,请稍后再试";
+
+}

+ 76 - 0
src/main/java/com/fdkankan/openApi/component/LimitAspect.java

@@ -0,0 +1,76 @@
+package com.fdkankan.openApi.component;
+
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.StrUtil;
+import com.fdkankan.common.constant.ErrorCode;
+import com.fdkankan.common.exception.BusinessException;
+import com.google.common.collect.ConcurrentHashMultiset;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.RateLimiter;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.reflect.MethodSignature;
+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 java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+@Slf4j
+@Aspect
+@Component
+public class LimitAspect {
+
+    private static ConcurrentHashMap<String, RateLimiter> limitMap = new ConcurrentHashMap<>();
+
+    @Before("@annotation(com.fdkankan.openApi.component.Limit)")
+    public  void before(JoinPoint joinPoint) throws Throwable {
+
+        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        HttpServletRequest request = requestAttributes.getRequest();
+        String appKey = request.getHeader("Authorization");
+        if (StrUtil.isEmpty(appKey)) {
+            throw new BusinessException(ErrorCode.AUTH_FAIL);
+        }
+
+        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+        Method method = signature.getMethod();
+        //拿limit的注解
+        Limit limit = method.getAnnotation(Limit.class);
+
+        if (limit != null) {
+            //key作用:不同的接口,不同的流量控制
+            RateLimiter rateLimiter;
+            //验证缓存是否有命中key
+            if (!limitMap.containsKey(appKey)) {
+                // 创建令牌桶
+                log.info("创建桶开始");
+                rateLimiter = RateLimiter.create(limit.permitsPerSecond(), 100L, TimeUnit.MILLISECONDS);
+                limitMap.put(appKey, rateLimiter);
+                log.info("新建了令牌桶={},容量={}", appKey, limit.permitsPerSecond());
+            }
+            rateLimiter = limitMap.get(appKey);
+            log.info("获取令牌桶成功");
+            // 拿令牌
+            boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit());
+            // 拿不到命令,直接返回异常提示
+            if (!acquire) {
+                log.info("令牌桶={},获取令牌失败", appKey);
+                throw new BusinessException(ErrorCode.SYSTEM_BUSY);
+            }
+
+        }
+    }
+
+}

+ 37 - 0
src/main/java/com/fdkankan/openApi/component/RedisLimit.java

@@ -0,0 +1,37 @@
+package com.fdkankan.openApi.component;
+
+import com.fdkankan.openApi.constant.LimitType;
+
+import java.lang.annotation.*;
+import java.util.concurrent.TimeUnit;
+
+@Target({ElementType.METHOD,ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+@Documented
+public @interface RedisLimit {
+
+    // 资源名称
+    String name() default "";
+
+    // 资源key
+    String key() default "";
+
+    /**
+     * 滑动窗口时间单位,默认 分钟
+     */
+    TimeUnit timeUnit() default TimeUnit.SECONDS;
+
+    // 时间
+    int period();
+
+    // 最多访问次数
+    int limitCount();
+
+    // 类型
+    LimitType limitType() default LimitType.APP_KEY;
+
+    // 提示信息
+    String msg() default "系统繁忙,请稍后再试";
+
+}

+ 116 - 0
src/main/java/com/fdkankan/openApi/component/RedisLimitAspect.java

@@ -0,0 +1,116 @@
+package com.fdkankan.openApi.component;
+
+import cn.hutool.core.util.StrUtil;
+import com.fdkankan.common.constant.ErrorCode;
+import com.fdkankan.common.exception.BusinessException;
+import com.fdkankan.openApi.constant.LimitType;
+import com.fdkankan.web.util.WebUtil;
+import com.google.common.collect.ImmutableList;
+import jodd.datetime.TimeUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.core.script.RedisScript;
+import org.springframework.scripting.support.ResourceScriptSource;
+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.lang.reflect.Method;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Aspect
+@Configuration
+public class RedisLimitAspect {
+
+    private final static String REDIS_LIMIT_KEY_PREFIX = "track:limit:";
+
+    @Autowired
+    private RedisTemplate redisTemplate;
+    @Resource
+    private RedisScript<Long> limitRedisScript;
+
+    @Before("@annotation(com.fdkankan.openApi.component.RedisLimit)")
+    public void before(JoinPoint joinPoint){
+        MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
+        Method method = methodSignature.getMethod();
+        RedisLimit redisLimit = method.getAnnotation(RedisLimit.class);
+        LimitType limitType = redisLimit.limitType();
+
+        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        HttpServletRequest request = requestAttributes.getRequest();
+        int period = redisLimit.period();
+        int limitCount = redisLimit.limitCount();
+        TimeUnit timeUnit = redisLimit.timeUnit();
+        String key = null;
+        switch (limitType){
+            case APP_KEY:
+                key = request.getHeader("Authorization");
+                if (StrUtil.isEmpty(key)) {
+                    throw new BusinessException(ErrorCode.AUTH_FAIL);
+                }
+                break;
+            case IP:
+                key = WebUtil.getIpAddress(request);
+                break;
+            case CUSTOMER:
+                key = redisLimit.key();
+                break;
+            default:
+                key = StringUtils.upperCase(method.getName());
+        }
+        boolean limited = this.shouldLimited(key, limitCount, period, timeUnit);
+        if(limited){
+            throw new BusinessException(ErrorCode.SYSTEM_BUSY);
+        }
+    }
+
+    private boolean shouldLimited(String key, long max, long timeout, TimeUnit timeUnit) {
+        // 最终的 key 格式为:
+        // limit:自定义key:IP
+        // limit:类名.方法名:IP
+        key = REDIS_LIMIT_KEY_PREFIX + key;
+        // 统一使用单位毫秒
+        long ttl = timeUnit.toMillis(timeout);
+        // 当前时间毫秒数
+        long now = Instant.now().toEpochMilli();
+        long expired = now - ttl;
+        /**
+         * 注意这里必须转为 String,否则会报错 java.lang.Long cannot be cast to java.lang.String
+         * stringRedisTemplate.execute(RedisScript<T> script, List<K> keys, Object... args)
+         */
+        Long executeTimes = (Long)redisTemplate.execute(limitRedisScript, Collections.singletonList(key), String.valueOf(now), String.valueOf(ttl), String.valueOf(expired), String.valueOf(max));
+        if (executeTimes != null) {
+            if (executeTimes == 0) {
+                return true;
+            } else {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    @Bean("limitRedisScript")
+    public RedisScript<Long> limitRedisScript() {
+        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
+        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redisScript/limit.lua")));
+        redisScript.setResultType(Long.class);
+        return redisScript;
+    }
+
+}

+ 27 - 0
src/main/java/com/fdkankan/openApi/constant/LimitType.java

@@ -0,0 +1,27 @@
+package com.fdkankan.openApi.constant;
+
+import lombok.AllArgsConstructor;
+
+@AllArgsConstructor
+public enum LimitType {
+
+    /**
+     * 应用维度
+     */
+    APP_KEY("appKey"),
+    /**
+     * 自定义维度
+     */
+    CUSTOMER("customer"),
+
+    /**
+     * ip维度
+     */
+    IP("ip"),
+    ;
+
+    private String code;
+
+
+
+}

+ 24 - 5
src/main/java/com/fdkankan/openApi/controller/www/SceneController.java

@@ -2,9 +2,13 @@ package com.fdkankan.openApi.controller.www;
 
 
 import cn.dev33.satoken.annotation.SaIgnore;
+import cn.hutool.http.HttpUtil;
+import com.alibaba.nacos.common.http.HttpUtils;
 import com.fdkankan.common.constant.ErrorCode;
 import com.fdkankan.common.exception.BusinessException;
 import com.fdkankan.openApi.common.PageInfo;
+import com.fdkankan.openApi.component.Limit;
+import com.fdkankan.openApi.component.RedisLimit;
 import com.fdkankan.openApi.component.ValidateApi;
 import com.fdkankan.openApi.controller.BaseController;
 import com.fdkankan.openApi.entity.www.ScenePlus;
@@ -13,13 +17,16 @@ import com.fdkankan.openApi.service.www.IScenePlusService;
 import com.fdkankan.openApi.vo.BaseSceneParamVo;
 import com.fdkankan.openApi.vo.www.PageScenesParamVo;
 import com.fdkankan.web.response.ResultData;
+import com.google.common.util.concurrent.RateLimiter;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.util.ObjectUtils;
 import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.TimeUnit;
 
 /**
  * <p>
@@ -29,6 +36,7 @@ import org.springframework.web.bind.annotation.RestController;
  * @author 
  * @since 2023-02-15
  */
+@Slf4j
 @RestController
 @RequestMapping("/scene")
 public class SceneController extends BaseController {
@@ -44,6 +52,18 @@ public class SceneController extends BaseController {
      * @return
      */
     @SaIgnore
+    @GetMapping("/test")
+    @RedisLimit(limitCount = 1, period = 1)
+//    @ValidateApi(method = "scene:getSceneList")
+    public  ResultData test() throws InterruptedException {
+        return ResultData.ok();
+    }
+
+    /**
+     * 获取场景列表
+     * @return
+     */
+    @SaIgnore
     @PostMapping("/getSceneList")
     @ValidateApi(method = "scene:getSceneList")
     public ResultData getScenesByUsername(@RequestBody PageScenesParamVo param) {
@@ -51,7 +71,6 @@ public class SceneController extends BaseController {
         return ResultData.ok(pageInfo);
     }
 
-
     /**
      * 获取点位信息
      * @return

+ 25 - 0
src/main/resources/redisScript/limit.lua

@@ -0,0 +1,25 @@
+local key = KEYS[1]
+local now = tonumber(ARGV[1]) -- 当前时时间戳
+local ttl = tonumber(ARGV[2]) -- key的过期时间
+local expired = tonumber(ARGV[3]) -- 元素过期分数上限(用于移除过期时间窗口元素)
+local limitCount = tonumber(ARGV[4]) --限制访问次数
+
+-- 清除过期的数据
+-- 移除指定分数区间内的所有元素,expired 即已经过期的 score
+-- 根据当前时间毫秒数 - 超时毫秒数,得到过期时间 expired
+redis.call('zremrangebyscore', key, 0, expired)
+
+-- 获取 zset 中的当前元素个数
+local current = tonumber(redis.call('zcard', key))
+local next = current + 1
+
+if next > max then
+  -- 达到限流大小 返回 0
+  return 0;
+else
+  -- 往 zset 中添加一个[value,score]均为当前时间戳的元素,[value,score]
+  redis.call("zadd", key, now, now)
+  -- 每次访问均重新设置 zset 的过期时间,单位毫秒
+  redis.call("pexpire", key, ttl)
+  return next
+end