SpringBoot实现限流注解
SpringBoot实现限流注解
在高并发系统中,保护系统的三种方式分别为:缓存,降级和限流。
限流的目的是通过对并发访问请求进行限速或者一个时间窗口内的的请求数量进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待
1、限流类型枚举类
/** * 限流类型 * @author ss_419 */ public enum LimitType { /** * 默认的限流策略,针对某一个接口进行限流 */ DEFAULT, /** * 针对某一个IP进行限流 */ IP }
2、自定义限流注解
/** * @author ss_419 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RateLimiter { /** * 限流的 key,主要是指前缀 * @return */ String key() default "rate_limit:"; /** * 在时间窗内的限流次数 * @return */ int count() default 100; /** * 限流类型 * @return */ LimitType limitType() default LimitType.DEFAULT; /** * 限流时间窗 * @return */ int time() default 60; }
3、限流lua脚本
1、由于我们使用 Redis 进行限流,我们需要引入 Redis 的 maven 依赖,同时需要引入 aop 的依赖
<!-- aop依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- redis依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2、配置redis以及lua脚本
@Configuration public class RedisConfig { @Bean RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); template.setKeySerializer(jackson2JsonRedisSerializer); template.setValueSerializer(jackson2JsonRedisSerializer); template.setHashKeySerializer(jackson2JsonRedisSerializer); template.setHashValueSerializer(jackson2JsonRedisSerializer); return template; } /** * 读取lua脚本 * @return */ @Bean DefaultRedisScript<Long> limitScript() { DefaultRedisScript<Long> script = new DefaultRedisScript<>(); script.setResultType(Long.class); script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua"))); return script; } }
通过 Lua 脚本,根据 Redis 中缓存的键值判断限流时间(也是 key 的过期时间)内,访问次数是否超出了限流次数,没超出则访问次数 +1,返回 true,超出了则返回 false。
limit.lua:
local key = KEYS[1] local time = tonumber(ARGV[1]) local count = tonumber(ARGV[2]) local current = redis.call('get', key) if current and tonumber(current) > count then return tonumber(current) end current = redis.call('incr', key) if tonumber(current) == 1 then redis.call('expire', key, time) end return tonumber(current)
4、限流切面处理类
1、使用我们刚刚的 Lua 脚本判断是否超出了限流次数,超出了限流次数后返回一个自定义异常,然后在全局异常中去捕捉异常,返回 JSON 数据。
2、根据注解参数,判断限流类型,拼接缓存 key 值
package org.pp.ratelimiter.aspectj; 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.pp.ratelimiter.annotation.RateLimiter; import org.pp.ratelimiter.enums.LimitType; import org.pp.ratelimiter.exception.RateLimitException; import org.pp.ratelimiter.utils.IpUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.lang.reflect.Method; import java.util.Collections; @Aspect @Component public class RateLimiterAspect { private static final Logger logger = LoggerFactory.getLogger(RateLimiterAspect.class); @Autowired RedisTemplate<Object, Object> redisTemplate; @Autowired RedisScript<Long> redisScript; @Before("@annotation(rateLimiter)") public void before(JoinPoint jp, RateLimiter rateLimiter) throws RateLimitException { int time = rateLimiter.time(); int count = rateLimiter.count(); String combineKey = getCombineKey(rateLimiter, jp); try { Long number = redisTemplate.execute(redisScript, Collections.singletonList(combineKey), time, count); if (number == null || number.intValue() > count) { //超过限流阈值 logger.info("当前接口以达到最大限流次数"); throw new RateLimitException("访问过于频繁,请稍后访问"); } logger.info("一个时间窗内请求次数:{},当前请求次数:{},缓存的 key 为 {}", count, number, combineKey); } catch (Exception e) { throw e; } } /** * 这个 key 其实就是接口调用次数缓存在 redis 的 key * rate_limit:11.11.11.11-org.javaboy.ratelimit.controller.HelloController-hello * rate_limit:org.javaboy.ratelimit.controller.HelloController-hello * @param rateLimiter * @param jp * @return */ private String getCombineKey(RateLimiter rateLimiter, JoinPoint jp) { StringBuffer key = new StringBuffer(rateLimiter.key()); if (rateLimiter.limitType() == LimitType.IP) { key.append(IpUtils.getIpAddr(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest())) .append("-"); } MethodSignature signature = (MethodSignature) jp.getSignature(); Method method = signature.getMethod(); key.append(method.getDeclaringClass().getName()) .append("-") .append(method.getName()); return key.toString(); } }
5、使用与测试
@RestController public class HelloController { /** * 限流 10 秒之内,这个接口可以访问3次 * @return */ @GetMapping("/hello") @RateLimiter(time = 10,count = 3) public Map<String, Object> hello() { Map<String, Object> map = new HashMap<>(); map.put("status", 200); map.put("message", "Hello RateLimiter"); return map; } }
十秒之内访问次数超过3次就会报异常
redis中的数据,每一次访问都加1
当访问次数超过3,则进行限流操作
posted on 2023-05-21 17:03 JavaCoderPan 阅读(473) 评论(0) 编辑 收藏 举报
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了