摘要:自定义注解实现基于接口限流仔细看会发现上面的简单实现会造成我每个接口都要写一次限流方法代码很冗余所以采用来使用自定义注解来实现。
服务限流 -- 自定义注解基于RateLimiter实现接口限流
令牌桶限流算法
图片来自网上
令牌桶会以一个恒定的速率向固定容量大小桶中放入令牌,当有浏览来时取走一个或者多个令牌,当发生高并发情况下拿到令牌的执行业务逻辑,没有获取到令牌的就会丢弃获取服务降级处理,提示一个友好的错误信息给用户。
2. RateLimiter简单实现
maven依赖
com.google.guava guava 18.0
本人使用的是SpringBoot 2.0.4.RELEASE,Jdk1.8环境下编写,部分代码贴出:
/** * 以1r/s往桶中放入令牌 */ private RateLimiter limiter = RateLimiter.create(1.0); private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @GetMapping("/indexLimiter") public String indexLimiter() { // 如果用户在500毫秒内没有获取到令牌,就直接放弃获取进行服务降级处理 boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MILLISECONDS); if (!tryAcquire) { log.info("Error ---时间:{},获取令牌失败.", sdf.format(new Date())); return "系统繁忙,请稍后再试."; } log.info("Success ---时间:{},获取令牌成功.", sdf.format(new Date())); return "success"; }
调用结果如下:
使用RateLimiter注意的地方:
允许先消费,后付款,意思就是它可以来一个请求的时候一次性取走几个或者是剩下所有的令牌甚至多取,但是后面的请求就得为上一次请求买单,它需要等待桶中的令牌补齐之后才能继续获取令牌。
3.自定义注解实现基于接口限流
仔细看会发现上面的简单实现会造成我每个接口都要写一次限流方法代码很冗余,所以采用aop来使用自定义注解来实现。
maven依赖
org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-web com.google.guava guava 18.0 org.projectlombok lombok true
首先定义一个自定义注解:
package com.limiting.annotation; import java.lang.annotation.*; import java.util.concurrent.TimeUnit; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface AnRateLimiter { //以固定数值往令牌桶添加令牌 double permitsPerSecond () ; //获取令牌最大等待时间 long timeout(); // 单位(例:分钟/秒/毫秒) 默认:毫秒 TimeUnit timeunit() default TimeUnit.MILLISECONDS; // 无法获取令牌返回提示信息 默认值可以自行修改 String msg() default "系统繁忙,请稍后再试."; }
然后使用aop的环绕通知来拦截注解,使用了一个ConcurrentMap来保存每个请求对应的令牌桶,key是没有url请求,防止出现每个请求都会新建一个令牌桶这么会达不到限流效果.
package com.limiting.aspect; import com.google.common.collect.Maps; import com.google.common.util.concurrent.RateLimiter; import com.limiting.annotation.AnRateLimiter; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Method; import java.util.Map; import java.util.Objects; /** * * 描述: * * @author 只写BUG的攻城狮 * * @date 2018-09-12 12:07 */ @Slf4j @Aspect @Component public class RateLimiterAspect { /** * 使用url做为key,存放令牌桶 防止每次重新创建令牌桶 */ private MaplimitMap = Maps.newConcurrentMap(); @Pointcut("@annotation(com.limiting.annotation.AnRateLimiter)") public void anRateLimiter() { } @Around("anRateLimiter()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { // 获取request,response HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); // 或者url(存在map集合的key) String url = request.getRequestURI(); // 获取自定义注解 AnRateLimiter rateLimiter = getAnRateLimiter(joinPoint); if (rateLimiter != null) { RateLimiter limiter = null; // 判断map集合中是否有创建有创建好的令牌桶 if (!limitMap.containsKey(url)) { // 创建令牌桶 limiter = RateLimiter.create(rateLimiter.permitsPerSecond()); limitMap.put(url, limiter); log.info("<<================= 请求{},创建令牌桶,容量{} 成功!!!", url, rateLimiter.permitsPerSecond()); } limiter = limitMap.get(url); // 获取令牌 boolean acquire = limiter.tryAcquire(rateLimiter.timeout(), rateLimiter.timeunit()); if (!acquire) { responseResult(response, 500, rateLimiter.msg()); return null; } } return joinPoint.proceed(); } /** * 获取注解对象 * @param joinPoint 对象 * @return ten LogAnnotation */ private AnRateLimiter getAnRateLimiter(final JoinPoint joinPoint) { Method[] methods = joinPoint.getTarget().getClass().getDeclaredMethods(); String name = joinPoint.getSignature().getName(); if (!StringUtils.isEmpty(name)) { for (Method method : methods) { AnRateLimiter annotation = method.getAnnotation(AnRateLimiter.class); if (!Objects.isNull(annotation) && name.equals(method.getName())) { return annotation; } } } return null; } /** * 自定义响应结果 * * @param response 响应 * @param code 响应码 * @param message 响应信息 */ private void responseResult(HttpServletResponse response, Integer code, String message) { response.resetBuffer(); response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); PrintWriter writer = null; try { writer = response.getWriter(); writer.println("{"code":" + code + " ,"message" :"" + message + ""}"); response.flushBuffer(); } catch (IOException e) { log.error(" 输入响应出错 e = {}", e.getMessage(), e); } finally { if (writer != null) { writer.flush(); writer.close(); } } } }
最后来试试自己定义的注解是否生效,能否达到限流效果.
@GetMapping("/index") @AnRateLimiter(permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "亲,现在流量过大,请稍后再试.") public String index() { return System.currentTimeMillis() + ""; }
访问请求(按F5狂刷新浏览器)效果如下图:
总结
至此已基本上使用注解实现了接口限流,后期可以根据自己需求自行修改,这个只适于单个应用进行接口限流,如果是分布式项目或者微服务项目可以采用redis来实现,后期有时间来一个基于redis自定义注解来实现接口限流。
本人也是刚入Java开发行业没多久的小菜鸟,在文章中可能存在一些说的不对,代码不严谨的地方欢迎各位大神指出,本人表示由衷的感谢和耐心的学习,希望能在开发中给大家一些帮助。
参考
https://crossoverjie.top/2017...
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/77105.html
摘要:计数限流算法无论固定窗口还是滑动窗口核心均是对请求进行计数,区别仅仅在于对于计数时间区间的处理。令牌桶限流实现原理令牌桶限流的实现原理在有详细说明。因此由此为入口进行分析。目前可返回的实现子类包括及两种,具体不同下文详细分析。 限流 限流一词常用于计算机网络之中,定义如下: In computer networks, rate limiting is used to control t...
摘要:令牌桶算法漏桶算法漏桶漏桶的出水速度是恒定的,那么意味着如果瞬时大流量的话,将有大部分请求被丢弃掉也就是所谓的溢出。 工作中对外提供的API 接口设计都要考虑限流,如果不考虑限流,会成系统的连锁反应,轻者响应缓慢,重者系统宕机,整个业务线崩溃,如何应对这种情况呢,我们可以对请求进行引流或者直接拒绝等操作,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机。 在开发高并发...
摘要:关于如何限速,有两个比较出名的算法,漏桶算法与令牌桶算法,这里对其简单介绍一下,最后再实践在我发邮件的中以下是发送邮件的,已限制为一分钟两次,你可以通过修改进行试验。 前段时间,我使用了 jwt 来实现邮箱验证码的校验与用户认证与登录,还特别写了一篇文章作为总结。 在那篇文章中,提到了一个点,如何限速。 在短信验证码和邮箱验证码,如果不限速,被恶意攻击造成大量的 QPS,不仅拖垮了服务...
阅读 2287·2021-11-22 14:56
阅读 1338·2021-09-24 09:47
阅读 858·2019-08-26 18:37
阅读 2787·2019-08-26 12:10
阅读 1477·2019-08-26 11:55
阅读 3100·2019-08-23 18:07
阅读 2253·2019-08-23 14:08
阅读 579·2019-08-23 12:12