Young Kbt blog Young Kbt blog
首页
  • java基础

    • Java基础
    • Java集合
    • Java反射
    • JavaJUC
    • JavaJVM
  • Java容器

    • JavaWeb
  • Java版本新特性

    • Java新特性
  • SQL 数据库

    • MySQL
    • Oracle
  • NoSQL 数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • ActiveMQ
    • RabbitMQ
    • RocketMQ
    • Kafka
  • 进阶服务

    • Nginx
  • Spring
  • Spring Boot
  • Spring Security
  • 设计模式
  • 算法
  • 知识
  • 管理

    • Maven
    • Git
  • 部署

    • Linux
    • Docker
    • Jenkins
    • Kubernetes
  • 进阶

    • TypeScript
  • 框架

    • React
    • Vue2
    • Vue3
  • 轮子工具
  • 项目工程
  • 友情链接
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 关于
    • Vue2-Admin (opens new window)
    • Vue3-Admin(完善) (opens new window)
GitHub (opens new window)

Shp Liu

朝圣的使徒,正在走向编程的至高殿堂!
首页
  • java基础

    • Java基础
    • Java集合
    • Java反射
    • JavaJUC
    • JavaJVM
  • Java容器

    • JavaWeb
  • Java版本新特性

    • Java新特性
  • SQL 数据库

    • MySQL
    • Oracle
  • NoSQL 数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • ActiveMQ
    • RabbitMQ
    • RocketMQ
    • Kafka
  • 进阶服务

    • Nginx
  • Spring
  • Spring Boot
  • Spring Security
  • 设计模式
  • 算法
  • 知识
  • 管理

    • Maven
    • Git
  • 部署

    • Linux
    • Docker
    • Jenkins
    • Kubernetes
  • 进阶

    • TypeScript
  • 框架

    • React
    • Vue2
    • Vue3
  • 轮子工具
  • 项目工程
  • 友情链接
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 关于
    • Vue2-Admin (opens new window)
    • Vue3-Admin(完善) (opens new window)
GitHub (opens new window)
  • 设计模式

  • 算法

  • 知识

    • 知识 - 对象
    • 知识 - 幂等性
    • 知识 - 分布式事务
    • 知识 - MapStruct
    • 知识 - MapStructPlus
    • 知识 - CompletableFuture
    • 知识 - EasyExcel
    • 知识 - Encrypt
    • 知识 - 接口幂等性
    • 知识 - 数据脱敏
    • 知识 - WebSocket
    • 知识 - Spring Cache
    • 知识 - 请求日志输出
    • 知识 - 接口限流
      • 前言
      • 实现
        • 依赖
        • 注解
        • 枚举
        • 配置项
        • AOP 切面
        • 容器装配
  • 开发
  • 知识
Young Kbt
2024-06-15
目录

知识 - 接口限流

  • 前言
  • 实现
    • 依赖
    • 注解
    • 枚举
    • 配置项
    • AOP 切面
    • 容器装配

# 前言

本内容介绍使用 Redis 对接口进行限流访问。

主要使用 AOP 对接口进行切入,然后 Before 时获取相关信息,存入 Redis,并设置失效时间,这样在没有失效前,访问接口时会返回失败信息。

# 实现

# 依赖

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
  </dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10

# 注解

/**
 * 例: @RedisLimit(key = "redis-limit:test", permitsPerSecond = 4, expire = 1, msg = "请求太频繁,等一下啦")
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface RateLimit {
    /**
     * 资源的 key,唯一
     * 作用:不同的接口,不同的流量控制
     */
    String key() default "";

    /**
     * 限流类型
     */
    RateLimitType rateLimitType() default RateLimitType.DEFAULT;

    /**
     * 最多的访问限制次数
     */
    long count() default 2;

    /**
     * 过期时间也可以理解为单位时间,单位秒,默认 60
     */
    long expire() default 60;

    /**
     * 得不到令牌的提示语
     */
    String msg() default "系统繁忙,请稍后再试!";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 枚举

public enum RateLimitType {
    /**
     * 默认策略全局限流
     */
    DEFAULT,

    /**
     * 根据用户 ID 限流
     */
    USER,

    /**
     * 根据请求者 IP 进行限流
     */
    IP,

    /**
     * 实例限流(集群多后端实例)
     */
    CLUSTER
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 配置项

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "redis-limit")
public class RateLimitProperties {
    
    /**
     * 访问次数
     */
    private Long limit;

    /**
     * 有效期,单位秒
     */
    private Long expire;

    /**
     * 限流 urls
     */
    private String[] urls;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# AOP 切面

@Slf4j
@Aspect
@RequiredArgsConstructor
public class RateLimitAspect {

    private final StringRedisTemplate stringRedisTemplate;
    private final RateLimitProperties rateLimitProperties;

    private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
    private static final TemplateParserContext TEMPLATE_PARSER_CONTEXT = new TemplateParserContext();

    @Pointcut("execution(* com..controller..*.*(..)) || @annotation(cn.youngkbt.ratelimiter.annotations.RateLimit)")
    private void check() {
    }

    private DefaultRedisScript<Long> redisScript;

    @PostConstruct
    public void init() {
        redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Long.class);
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));
    }

    @Before("check()")
    public void before(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        // 拿到 RedisLimit 注解,如果存在则说明需要限流
        RateLimit rateLimit = method.getAnnotation(RateLimit.class);
        HttpServletRequest request = WebUtil.getRequest();
        if (Objects.isNull(request)) {
            return;
        }
        // StrUtil.containsAnyIgnoreCase 判断请求的 url 是否有配置文件限流的 urls
        if (Objects.nonNull(rateLimit) || StrUtil.containsAnyIgnoreCase(request.getRequestURI(), rateLimitProperties.getUrls())) {
            // 获取 redis 的 key
            String key;
            long limit;
            long expire;
            if (Objects.nonNull(rateLimit)) {
                key = getLimitKey(rateLimit.key(), rateLimit.rateLimitType(), joinPoint);
                limit = rateLimit.count();
                expire = rateLimit.expire();
            } else {
                // 根据 URI + 用户 ID 限流
                key = getLimitKey(request.getRequestURI(), RateLimitType.USER, joinPoint);
                limit = rateLimitProperties.getLimit();
                expire = rateLimitProperties.getExpire();
            }
            if (!StringUtil.hasText(key)) {
                throw new RedisLimitException("redis key cannot be null");
            }

            List<String> keys = new ArrayList<>();
            keys.add(key);
            Long count = stringRedisTemplate.execute(redisScript, keys, String.valueOf(limit), String.valueOf(expire));
            log.info("接口限流, 尝试访问次数为 {},key:{}", count, key);

            if (Objects.nonNull(count) && count == 0) {
                log.debug("接口限流, 导致获取 key 失败,key 为 {}", key);
                throw new RedisLimitException(Objects.nonNull(rateLimit) ? rateLimit.msg() : "请求过于频繁!");
            }
        }
    }

    private String getLimitKey(String key, RateLimitType rateLimitType, JoinPoint joinPoint) {
        StringBuilder stringBuffer = new StringBuilder(RedisConstants.SERVER_REQUEST_LIMIT);

        if (StringUtil.hasText(key)) {
            key = getSpeElValue(key, joinPoint);
        }

        if (rateLimitType == RateLimitType.USER) {
            // 获取用户 ID
            stringBuffer.append(LoginHelper.getUsername()).append(":");
        } else if (rateLimitType == RateLimitType.IP) {
            // 获取请求 IP
            stringBuffer.append(ServletUtil.getClientIp()).append(":");
        } else if (rateLimitType == RateLimitType.CLUSTER) {
            // 获取客户端实例 ID
            // stringBuffer.append(getClientId()).append(":");
        }

        return stringBuffer.append(key).toString();
    }

    private String getSpeElValue(String key, JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method targetMethod = signature.getMethod();
        // 方法的参数
        Object[] args = joinPoint.getArgs();

        // 创建 MethodBasedEvaluationContext
        MethodBasedEvaluationContext context = new MethodBasedEvaluationContext("", targetMethod, args, new DefaultParameterNameDiscoverer());

        // 设置 ApplicationContext 到 Context 中
        context.setBeanResolver(new BeanFactoryResolver(SpringHelper.getBeanFactory()));

        Expression expression = null;
        // 如果 key 为 SpEl 表达式
        if (StringUtils.startsWith(key, TEMPLATE_PARSER_CONTEXT.getExpressionPrefix())
                && StringUtils.endsWith(key, TEMPLATE_PARSER_CONTEXT.getExpressionSuffix())) {
            expression = EXPRESSION_PARSER.parseExpression(key, TEMPLATE_PARSER_CONTEXT);
        } else if (StringUtils.startsWith(key, "#")) {
            expression = EXPRESSION_PARSER.parseExpression(key);
        }
        return Objects.nonNull(expression) ? expression.getValue(context, String.class) : key;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110

# 容器装配

@AutoConfiguration(after = RedisConfiguration.class)
@EnableConfigurationProperties(RateLimitProperties.class)
public class RateLimitConfiguration {

    @Bean
    public RateLimitAspect rateLimiterAspect(StringRedisTemplate stringRedisTemplate, RateLimitProperties rateLimitProperties) {
        return new RateLimitAspect(stringRedisTemplate, rateLimitProperties);
    }
}
1
2
3
4
5
6
7
8
9

Spring Boot 3.x 需要在 resource 下建立 META-INF/spring 路径,然后创建 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,内容为

cn.youngkbt.ratelimiter.config.RateLimitConfiguration
1

这样 Spring 会自动扫描该文件的容器装配类,将里面涉及的类注入到 Spring 容器。

编辑此页 (opens new window)
#knowledge
更新时间: 2024/06/15, 16:39:27
知识 - 请求日志输出

← 知识 - 请求日志输出

最近更新
01
技术随笔 - Element Plus 修改包名 原创
11-02
02
Reactor - 扩展性
11-02
03
Reactor - 最佳实践
11-02
更多文章>
Theme by Vdoing | Copyright © 2021-2024 Young Kbt | blog
桂ICP备2021009994号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式