广告位联系
返回顶部
分享到

使用AOP+redis+lua做方法限流的实现

Redis 来源:互联网 作者:秩名 发布时间:2022-04-29 21:25:37 人浏览
摘要

需求 公司里使用OneByOne的方式删除数据,为了防止一段时间内删除数据过多,让我这边做一个接口限流,超过一定阈值后报异常,终止删除操作。 实现方式 创建自定义注解@limit让使用

需求

公司里使用OneByOne的方式删除数据,为了防止一段时间内删除数据过多,让我这边做一个接口限流,超过一定阈值后报异常,终止删除操作。

实现方式

创建自定义注解 @limit 让使用者在需要的地方配置 count(一定时间内最多访问次数)、 period(给定的时间范围),也就是访问频率。然后通过LimitInterceptor拦截方法的请求, 通过 redis+lua 脚本的方式,控制访问频率。

源码

Limit 注解

用于配置方法的访问频率count、period

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

import javax.validation.constraints.Min;

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

@Inherited

@Documented

public @interface Limit {

    /**

     * key

     */

    String key() default "";

    /**

     * Key的前缀

     */

    String prefix() default "";

    /**

     * 一定时间内最多访问次数

     */

    @Min(1)

    int count();

    /**

     * 给定的时间范围 单位(秒)

     */

    @Min(1)

    int period();

    /**

     * 限流的类型(用户自定义key或者请求ip)

     */

    LimitType limitType() default LimitType.CUSTOMER;

}

LimitKey

用于标记参数,作为redis key值的一部分

1

2

3

4

5

6

7

8

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)

@Retention(RetentionPolicy.RUNTIME)

public @interface LimitKey {

}

LimitType

枚举,redis key值的类型,支持自定义key和ip、methodName中获取key

1

2

3

4

5

6

7

8

9

10

11

12

13

14

public enum LimitType {

    /**

     * 自定义key

     */

    CUSTOMER,

    /**

     * 请求者IP

     */

    IP,

    /**

     * 方法名称

     */

    METHOD_NAME;

}

RedisLimiterHelper

初始化一个限流用到的redisTemplate Bean

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;

import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.io.Serializable;

@Configuration

public class RedisLimiterHelper {

    @Bean

    public RedisTemplate<String, Serializable> limitRedisTemplate(@Qualifier("defaultStringRedisTemplate") StringRedisTemplate redisTemplate) {

        RedisTemplate<String, Serializable> template = new RedisTemplate<String, Serializable>();

        template.setKeySerializer(new StringRedisSerializer());

        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        template.setConnectionFactory(redisTemplate.getConnectionFactory());

        return template;

    }

}

LimitInterceptor

使用 aop 的方式来拦截请求,控制访问频率

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

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

import com.google.common.collect.ImmutableList;

import com.yxt.qida.api.bean.service.xxv2.openapi.anno.Limit;

import com.yxt.qida.api.bean.service.xxv2.openapi.anno.LimitKey;

import com.yxt.qida.api.bean.service.xxv2.openapi.anno.LimitType;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.ArrayUtils;

import org.apache.commons.lang3.StringUtils;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.reflect.MethodSignature;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Configuration;

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.web.context.request.RequestContextHolder;

import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

import java.io.Serializable;

import java.lang.annotation.Annotation;

import java.lang.reflect.Method;

@Slf4j

@Aspect

@Configuration

public class LimitInterceptor {

    private static final String UNKNOWN = "unknown";

    private final RedisTemplate<String, Serializable> limitRedisTemplate;

    @Autowired

    public LimitInterceptor(RedisTemplate<String, Serializable> limitRedisTemplate) {

        this.limitRedisTemplate = limitRedisTemplate;

    }

    @Around("execution(public * *(..)) && @annotation(com.yxt.qida.api.bean.service.xxv2.openapi.anno.Limit)")

    public Object interceptor(ProceedingJoinPoint pjp) {

        MethodSignature signature = (MethodSignature) pjp.getSignature();

        Method method = signature.getMethod();

        Limit limitAnnotation = method.getAnnotation(Limit.class);

        LimitType limitType = limitAnnotation.limitType();

        int limitPeriod = limitAnnotation.period();

        int limitCount = limitAnnotation.count();

        /**

         * 根据限流类型获取不同的key ,如果不传我们会以方法名作为key

         */

        String key;

        switch (limitType) {

            case IP:

                key = getIpAddress();

                break;

            case CUSTOMER:

                key = limitAnnotation.key();

                break;

            case METHOD_NAME:

                String methodName = method.getName();

                key = StringUtils.upperCase(methodName);

                break;

            default:

                throw new RuntimeException("limitInterceptor - 无效的枚举值");

        }

        /**

         * 获取注解标注的 key,这个是优先级最高的,会覆盖前面的 key 值

         */

        Object[] args = pjp.getArgs();

        Annotation[][] paramAnnoAry = method.getParameterAnnotations();

        for (Annotation[] item : paramAnnoAry) {

            int paramIndex = ArrayUtils.indexOf(paramAnnoAry, item);

            for (Annotation anno : item) {

                if (anno instanceof LimitKey) {

                    Object arg = args[paramIndex];

                    if (arg instanceof String && StringUtils.isNotBlank((String) arg)) {

                        key = (String) arg;

                        break;

                    }

                }

            }

        }

        if (StringUtils.isBlank(key)) {

            throw new RuntimeException("limitInterceptor - key值不能为空");

        }

        String prefix = limitAnnotation.prefix();

        String[] keyAry = StringUtils.isBlank(prefix) ? new String[]{"limit", key} : new String[]{"limit", prefix, key};

        ImmutableList<String> keys = ImmutableList.of(StringUtils.join(keyAry, "-"));

        try {

            String luaScript = buildLuaScript();

            RedisScript<Number> redisScript = new DefaultRedisScript<Number>(luaScript, Number.class);

            Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);

            if (count != null && count.intValue() <= limitCount) {

                return pjp.proceed();

            } else {

                String classPath = method.getDeclaringClass().getName() + "." + method.getName();

                throw new RuntimeException("limitInterceptor - 限流被触发:"

                        + "class:" + classPath

                        + ", keys:" + keys

                        + ", limitcount:" + limitCount

                        + ", limitPeriod:" + limitPeriod + "s");

            }

        } catch (Throwable e) {

            if (e instanceof RuntimeException) {

                throw new RuntimeException(e.getLocalizedMessage());

            }

            throw new RuntimeException("limitInterceptor - 限流服务异常");

        }

    }

    /**

     * lua 脚本,为了保证执行 redis 命令的原子性

     */

    public String buildLuaScript() {

        StringBuilder lua = new StringBuilder();

        lua.append("local c");

        lua.append("\nc = redis.call('get',KEYS[1])");

        // 调用不超过最大值,则直接返回

        lua.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then");

        lua.append("\nreturn c;");

        lua.append("\nend");

        // 执行计算器自加

        lua.append("\nc = redis.call('incr',KEYS[1])");

        lua.append("\nif tonumber(c) == 1 then");

        // 从第一次调用开始限流,设置对应键值的过期

        lua.append("\nredis.call('expire',KEYS[1],ARGV[2])");

        lua.append("\nend");

        lua.append("\nreturn c;");

        return lua.toString();

    }

    public String getIpAddress() {

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        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.getRemoteAddr();

        }

        return ip;

    }

}

TestService

使用方式示例

1

2

3

4

@Limit(period = 10, count = 10)

public String delUserByUrlTest(@LimitKey String token, String thirdId, String url) throws IOException {

    return "success";

}


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 : https://juejin.cn/post/7091220805853872158
相关文章
  • spring boot集成redis基础入门实例介绍
    redis 支持持久化数据,不仅支持key-value类型的数据,还拥有list,set,zset,hash等数据结构的存储。 可以进行master-slave模式的数据备份 更多
  • redis批量操作pipeline管道操作方法

    redis批量操作pipeline管道操作方法
    redis | pipeline(管道) 背景 Redis是一种基于客户端-服务端模型以及请求/响应的TCP服务。这意味着通常情况下一个请求会遵循以下步骤: 客户
  • springboot整合使用云服务器上的Redis方法

    springboot整合使用云服务器上的Redis方法
    一、前提条件 修改redis.conf配置文件 1、protected-mode yes(默认的) 修改成 protected-mode no,解除保护模式 2、注释掉绑定ip ,绑定ip的话,使得
  • 阿里云服务器部署Redis并整合Spring Boot的介绍

    阿里云服务器部署Redis并整合Spring Boot的介绍
    一、什么是Redis redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zs
  • 生产redisson延时队列不消费问题排查解决

    生产redisson延时队列不消费问题排查解决
    问题描述 项目使用redisson延时队列功能,实现直播的开播提醒,突然有一天业务爆出问题,未触发开播提醒。 初步排查 首先通过查询生产日
  • Redis主从复制分步讲解使用

    Redis主从复制分步讲解使用
    主服务器(master)启用二进制日志 选择一个唯一的server-id 创建具有复制权限的用户 从服务器(slave)启用中继日志, 选择一个唯一的serv
  • Redis中HyperLogLog的使用介绍
    HyperLogLog,基数统计; 那什么是基数? 比如有两个数组 数组A = [1,2,3,4,5]; 数组B = [3,4,5,6,7]; 这时候基数就是[1,2,3,4,5,6,7],总共有7个数; 就是
  • Redis中的持久化介绍

    Redis中的持久化介绍
    1. 前言 为什么要进行持久化?:持久化功能有效地避免因进程退出造成的数据丢失问题,当下次重启时利用之前持久化的文件即可实现数据
  • Redis源码设计剖析之事件处理示例介绍

    Redis源码设计剖析之事件处理示例介绍
    1. Redis事件介绍 Redis服务器是一个 事件驱动程序 ,所谓事件驱动就是输入一条命令并且按下回车,然后消息被组装成 Redis 协议的格式发送给
  • Mysql应用安装后找不到my.ini文件的解决过程

    Mysql应用安装后找不到my.ini文件的解决过程
    一、背景 我在两台电脑上安装了MySQL Server 8.0,准备继续做主从配置,这时候就需要用到my.ini文件进行配置,但是我找不到my.ini文件。 我的
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计