服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

Mysql|Sql Server|Oracle|Redis|MongoDB|PostgreSQL|Sqlite|DB2|mariadb|Access|数据库技术|

服务器之家 - 数据库 - Redis - 使用AOP+redis+lua做方法限流的实现

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

2022-10-17 15:14竹子竹枝 Redis

本文主要介绍了使用AOP+redis+lua做方法限流的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

需求

公司里使用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";
}

到此这篇关于使用AOP+redis+lua做方法限流的实现的文章就介绍到这了,更多相关AOP+redis+lua限流内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://juejin.cn/post/7091220805853872158

延伸 · 阅读

精彩推荐
  • Redisredis集群搭建过程(非常详细,适合新手)

    redis集群搭建过程(非常详细,适合新手)

    这篇文章主要介绍了redis集群搭建过程,Redis集群至少需要3个节点,因为投票容错机制要求超过半数节点认为某个节点挂了该节点才是挂了,所以2个节点无法...

    凉凉的西瓜8412021-11-24
  • RedisRedis自动化安装及集群实现搭建过程

    Redis自动化安装及集群实现搭建过程

    这篇文章主要介绍了Redis自动化安装以及集群实现搭建过程,本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下 ...

    mrr1952019-11-28
  • Redis浅谈redis key值内存消耗以及性能影响

    浅谈redis key值内存消耗以及性能影响

    这篇文章主要介绍了浅谈redis key值内存消耗以及性能影响,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    Dark_King_5192021-08-06
  • Redisredis禁止几个危险命令的方法

    redis禁止几个危险命令的方法

    今天小编就为大家分享一篇redis禁止几个危险命令的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧 ...

    jjwen4082019-11-14
  • Redis硬核!15张图解Redis为什么这么快(推荐)

    硬核!15张图解Redis为什么这么快(推荐)

    作为一名服务端工程师,工作中你肯定和 Redis 打过交道。Redis为什么快,这点想必你也知道,至少为了面试也做过准备,今天通过本文给大家介绍下,感兴...

    呃呃呃额5962020-12-26
  • Redis详细分析Redis集群故障

    详细分析Redis集群故障

    这篇文章主要介绍了详细分析Redis集群故障的相关内容,具有一定的参考价值,这里分享给大家,供需要的朋友参考。 ...

    带鱼兄6772019-11-08
  • Redis基于Redis延迟队列的实现代码

    基于Redis延迟队列的实现代码

    在生活中很多时候都会用到延迟队列,本文基于Redis延迟队列的实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    程序员老郑12002021-08-04
  • Redis详解redis分布式锁的这些坑

    详解redis分布式锁的这些坑

    在很多互联网产品应用中,有些场景需要加锁处理,比如:秒杀,全局递增ID等等。大部分的解决方案是基于DB实现的,Redis为单进程单线程模式,采用队列...

    华为云开发者社区4282021-08-04