springboot 接口限流,
00-1010 1.准备2。限流注释3。自定义重新模板4。开发Lua脚本5。注释解析6。界面测试7。全局异常处理。
00-1010首先,我们创建一个Spring Boot项目,并介绍Web和Redis依赖项。同时考虑到接口限流一般都是用标注来标记的,而标注是用AOP来解析的,我们还需要添加AOP依赖。最终的依赖关系如下:
依赖groupIdorg.springframework.boot/groupId artifactId Spring-Boot-Starter-data-redis/artifactId/Dependency依赖groupIdorg.springframework.boot/groupId artifactId Spring-Boot-Starter-web/artifactId/Dependency依赖groupIdorg.springframework.boot/groupId工件Spring-Boot-Starter-AOP/工件Id/Dependency然后提前准备一个Redis的实例,我们的项目在这里配置好之后,直接配置Redis的基本信息就可以了,如下:
spring . redis . host=localhostspring . redis . port=6379 spring . redis . password=123
好了,准备工作已经就绪。
00-1010接下来,让我们创建一个电流限制注释。我们将电流限制分为两种情况:
当前接口的全局电流限制,例如,该接口在1分钟内可被访问100次。一个IP地址的电流限制,例如,一个IP地址可以在一分钟内被访问100次。对于这两种情况,我们创建一个枚举类:
Public enum LimitType {/** *默认策略完全限制流量*/default,/**根据请求者的IP限制流量*/IP}接下来,让我们创建一个限制注释:
@ target(element type . method)@ retention(retention policy . runtime)@ documented public @ interface速率限制器{/* * *限流键*/stringkey()默认为“rate _ limit 3360”;/* * *限流时间,单位为秒*/int time()默认60;/* * *限流次数*/int count()默认100;/* * *限流类型*/limittypelimittype()defaultlimittype . default;}第一个参数,限流的关键,只是一个前缀。以后完整的key就是这个前缀加上接口方法的完整路径,就形成了当前的limit key,这个key存放在Redis中。
其他三个参数比较好理解,就不多说了。
好了,以后只要给任何需要限流的接口加上@RateLimiter注释,然后配置相关参数就可以了。
00-1010的朋友都知道,在Spring Boot,我们其实更习惯用Spring Data Redis来操作Redis,但是默认的RedisTemplate有个小坑,就是用JDKSerialization Redisserizer来序列化。不知道朋友们有没有注意到,直接用这个序列化工具存储在Redis中的键和值会莫名其妙地加上前缀,可能会导致你用命令读取时出错。
比如存储的时候,键是name,值是javaboy,但是在命令行操作的时候,get name无法获取想要的数据。原因是保存到redis后,name前面有一些字符。这时候你只能继续用RedisTemplate把它读出来。
p>我们用 Redis 做限流会用到 Lua 脚本,使用 Lua 脚本的时候,就会出现上面说的这种情况,所以我们需要修改 RedisTemplate 的序列化方案。
可能有小伙伴会说为什么不用 StringRedisTemplate 呢?StringRedisTemplate 确实不存在上面所说的问题,但是它能够存储的数据类型不够丰富,所以这里不考虑。
修改 RedisTemplate 序列化方案,代码如下:
@Configurationpublic class RedisConfig { @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); // 使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化) Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); redisTemplate.setKeySerializer(jackson2JsonRedisSerializer); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); return redisTemplate; }}
这个其实也没啥好说的,key 和 value 我们都使用 Spring Boot 中默认的 jackson 序列化方式来解决。
4. 开发 Lua 脚本
Redis 中的一些原子操作我们可以借助 Lua 脚本来实现,想要调用 Lua 脚本,我们有两种不同的思路:
在 Redis 服务端定义好 Lua 脚本,然后计算出来一个散列值,在 Java 代码中,通过这个散列值锁定要执行哪个 Lua 脚本。直接在 Java 代码中将 Lua 脚本定义好,然后发送到 Redis 服务端去执行。Spring Data Redis 中也提供了操作 Lua 脚本的接口,还是比较方便的,所以我们这里就采用第二种方案。
我们在 resources 目录下新建 lua 文件夹专门用来存放 lua 脚本,脚本内容如下:
local key = KEYS[1]local count = tonumber(ARGV[1])local time = tonumber(ARGV[2])local current = redis.call('get', key)if current and tonumber(current) > count then return tonumber(current)endcurrent = redis.call('incr', key)if tonumber(current) == 1 then redis.call('expire', key, time)endreturn tonumber(current)
这个脚本其实不难,大概瞅一眼就知道干啥用的。KEYS 和 ARGV 都是一会调用时候传进来的参数,tonumber 就是把字符串转为数字,redis.call 就是执行具体的 redis 指令,具体流程是这样:
首先获取到传进来的 key 以及 限流的 count 和时间 time。通过 get 获取到这个 key 对应的值,这个值就是当前时间窗内这个接口可以访问多少次。如果是第一次访问,此时拿到的结果为 nil,否则拿到的结果应该是一个数字,所以接下来就判断,如果拿到的结果是一个数字,并且这个数字还大于 count,那就说明已经超过流量限制了,那么直接返回查询的结果即可。如果拿到的结果为 nil,说明是第一次访问,此时就给当前 key 自增 1,然后设置一个过期时间。最后把自增 1 后的值返回就可以了。其实这段 Lua 脚本很好理解。
接下来我们在一个 Bean 中来加载这段 Lua 脚本,如下:
@Beanpublic DefaultRedisScript<Long> limitScript() { DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua"))); redisScript.setResultType(Long.class); return redisScript;}
我们的 Lua 脚本现在就准备好了。
5. 注解解析
接下来我们就需要自定义切面,来解析这个注解了,我们来看看切面的定义:
@Aspect@Componentpublic class RateLimiterAspect { private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class); @Autowired private RedisTemplate<Object, Object> redisTemplate; @Autowired private RedisScript<Long> limitScript; @Before("@annotation(rateLimiter)") public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable { String key = rateLimiter.key(); int time = rateLimiter.time(); int count = rateLimiter.count(); String combineKey = getCombineKey(rateLimiter, point); List<Object> keys = Collections.singletonList(combineKey); try { Long number = redisTemplate.execute(limitScript, keys, count, time); if (number==null number.intValue() > count) { throw new ServiceException("访问过于频繁,请稍候再试"); } log.info("限制请求{},当前请求{},缓存key{}", count, number.intValue(), key); } catch (ServiceException e) { throw e; } catch (Exception e) { throw new RuntimeException("服务器限流异常,请稍候再试"); } } public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) { StringBuffer stringBuffer = new StringBuffer(rateLimiter.key()); if (rateLimiter.limitType() == LimitType.IP) { stringBuffer.append(IpUtils.getIpAddr(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest())).append("-"); } MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); Class<?> targetClass = method.getDeclaringClass(); stringBuffer.append(targetClass.getName()).append("-").append(method.getName()); return stringBuffer.toString(); }}
这个切面就是拦截所有加了@RateLimiter
注解的方法,在前置通知中对注解进行处理。
首先获取到注解中的 key、time 以及 count 三个参数。获取一个组合的 key,所谓的组合的 key,就是在注解的 key 属性基础上,再加上方法的完整路径,如果是 IP 模式的话,就再加上 IP 地址。以 IP 模式为例,最终生成的 key 类似这样:rate_limit:127.0.0.1-org.javaboy.ratelimiter.controller.HelloController-hello
(如果不是 IP 模式,那么生成的 key 中就不包含 IP 地址)。将生成的 key 放到集合中。通过 redisTemplate.execute 方法取执行一个 Lua 脚本,第一个参数是脚本所封装的对象,第二个参数是 key,对应了脚本中的 KEYS,后面是可变长度的参数,对应了脚本中的 ARGV。将 Lua 脚本执行的结果与 count 进行比较,如果大于 count,就说明过载了,抛异常就行了。好了,大功告成了。
6. 接口测试
接下来我们就进行接口的一个简单测试,如下:
@RestControllerpublic class HelloController { @GetMapping("/hello") @RateLimiter(time = 5,count = 3,limitType = LimitType.IP) public String hello() { return "hello>>>"+new Date(); }}
每一个 IP 地址,在 5 秒内只能访问 3 次。
这个自己手动刷新浏览器都能测试出来。
7. 全局异常处理
由于过载的时候是抛异常出来,所以我们还需要一个全局异常处理器,如下:
@RestControllerAdvicepublic class GlobalException { @ExceptionHandler(ServiceException.class) public Map<String,Object> serviceException(ServiceException e) { HashMap<String, Object> map = new HashMap<>(); map.put("status", 500); map.put("message", e.getMessage()); return map; }}
这是一个小 demo,我就不去定义实体类了,直接用 Map 来返回 JSON 了。
好啦,大功告成。
最后我们看看过载时的测试效果:
好啦,这就是我们使用 Redis 做限流的方式。
到此这篇关于SpringBoot Redis用注释实现接口限流详解的文章就介绍到这了,更多相关SpringBoot Redis接口限流内容请搜索盛行IT以前的文章或继续浏览下面的相关文章希望大家以后多多支持盛行IT!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。