点击上方“Java精选”,选择“设为星标”
别问别人为什么,多问自己凭什么!
下方有惊喜,留言必回,有问必答!
每天08:15更新文章,每天进步一点点...
令牌桶
在高并发的情况下,限流是后端常用的手段之一,可以对系统限流、接口限流、用户限流等,本文就使用令牌桶算法+拦截器+自定义注解+自定义异常实现限流的demo。
令牌桶思想
大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。
后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。然后每个访问的用户都会从中取走一块令牌,取到了令牌才能访问,如果没取到令牌即代表已达到访问上限,将被限流不允许访问
限流demo实现思路
创建令牌桶类
项目启动初始化令牌桶,并设置定时器,定时向桶内放入令牌
自定义限流注解,在需要限流的接口上打上注解
配置令牌桶拦截器,对所有路径进行拦截,对无限流注解的接口直接放行,对有限流注解的做取令牌处理,取到令牌即放行,没取到令牌即抛出自定义异常
自定义异常并使用AOP做全局异常处理
创建令牌类
这里为了防止并发问题在生成令牌和取令牌的方法上加了synchronized
BucketUtil如下
publicclassBucketUtil{//默认容量10staticfinalintDEFAULT_MAX_COUNT=10;//默认增长速率为1staticfinalintDEFAULT_CREATE_RATE=1;//使用HashMap存放令牌桶,这里默认为10个令牌桶publicstaticHashMap<String,BucketUtil>buckets=newHashMap(10);//自定义容量,一旦创建不可改变finalintmaxCount;//自定义增长速率1s几个令牌intcreateRate;//当前令牌数intsize=0;//默认令牌桶的容量及增长速率publicBucketUtil(){maxCount=DEFAULT_MAX_COUNT;createRate=DEFAULT_CREATE_RATE;}//自定义令牌桶容量及增长速率publicBucketUtil(intmaxCount,intcreateRate){this.maxCount=maxCount;this.createRate=createRate;}publicintgetSize(){returnsize;}publicbooleanisFull(){returnsize==maxCount;}//根据速率自增生成一个令牌publicsynchronizedvoidincrTokens(){for(inti=0;i<createRate;i++){if(isFull())return;size++;}}//取一个令牌publicsynchronizedbooleangetToken(){if(size>0)size--;elsereturnfalse;returntrue;}@Overridepublicbooleanequals(Objectobj){if(obj==null)returnfalse;BucketUtilbucket=(BucketUtil)obj;if(bucket.size!=size||bucket.createRate!=createRate||bucket.maxCount!=maxCount)returnfalse;returntrue;}@OverridepublicinthashCode(){returnObjects.hash(maxCount,size,createRate);}}
初始化令牌桶
在启动类上初始化并生成定时器
推荐下自己几个月熬夜整理的面试资料大全:
/yoodb/ebooks
@EnableScheduling@SpringBootApplicationpublicclassDemoApplication{publicstaticvoidmain(String[]args){SpringApplication.run(DemoApplication.class,args);//为了方便测试这里定义1容量1增长速率BucketUtilbucketUtil=newBucketUtil(1,1);//生成名为:bucket的令牌桶BucketUtil.buckets.put("bucket",bucketUtil);}@Scheduled(fixedRate=1000)//定时1spublicvoidtimer(){if(BucketUtil.buckets.containsKey("bucket")){//名为:bucket的令牌桶开始不断生成令牌BucketUtil.buckets.get("bucket").incrTokens();}}}
自定义注解以及异常
BucketAnnotation注解
@Target({ElementType.METHOD})//METHOD代表是用在方法上@Retention(RetentionPolicy.RUNTIME)public@interfaceBucketAnnotation{}
APIException 异常
publicclassAPIExceptionextendsRuntimeException{privatestaticfinallongserialVersionUID=1L;privateStringmsg;publicAPIException(Stringmsg){super(msg);this.msg=msg;}}
配置拦截器
BucketInterceptor拦截器如下
/***令牌桶拦截器*/publicclassBucketInterceptorimplementsHandlerInterceptor{//预处理回调方法,在接口调用之前使用true代表放行false代表不放行@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){if(!(handlerinstanceofHandlerMethod)){returntrue;}HandlerMethodhandlerMethod=(HandlerMethod)handler;Methodmethod=handlerMethod.getMethod();BucketAnnotationmethodAnnotation=method.getAnnotation(BucketAnnotation.class);if(methodAnnotation!=null){//在名为:bucket的令牌桶里取令牌取到即放行未取到即抛出异常if(BucketUtil.buckets.get("bucket").getToken()){returntrue;}else{//抛出自定义异常thrownewAPIException("不好意思,您被限流了");}}else{returntrue;}}//接口调用之后,返回之前使用@OverridepublicvoidpostHandle(HttpServletRequesthttpServletRequest,HttpServletResponsehttpServletResponse,Objecto,ModelAndViewmodelAndView)throwsException{}//整个请求完成后,在视图渲染前使用@OverridepublicvoidafterCompletion(HttpServletRequesthttpServletRequest,HttpServletResponsehttpServletResponse,Objecto,Exceptione)throwsException{}}
将拦截器注入
@ConfigurationpublicclassWebMvcConfgimplementsWebMvcConfigurer{@OverridepublicvoidaddInterceptors(InterceptorRegistryregistry){//令牌桶拦截器添加拦截器并选择拦截路径registry.addInterceptor(bucketInterceptor()).addPathPatterns("/**");}@BeanpublicBucketInterceptorbucketInterceptor(){returnnewBucketInterceptor();}}
AOP全局异常处理
@RestControllerAdvicepublicclassWebExceptionControl{@ExceptionHandler(APIException.class)publicE3ResultAPIExceptionHandler(APIExceptione){returnE3Result.build(400,e.getMessage());}}
测试
在我们需要限流的接口上打上自定义注解,如下
@BucketAnnotation@RequestMapping(value="/bucket")publicE3Resultbucket(){returnE3Result.ok("访问成功");}
关于E3Result只是一个封装好的返回类,这里就不贴出来了,大家有的替换成自己的,没有的可以直接用String型测试
推荐下自己做的 Spring boot 的实战项目:
/yoodb/jing-xuan
上面为了方便测试,令牌桶的容量设置成了1,所以这是取到令牌成功的
这是没取到令牌被限流的
总结
上面的限流只是一个demo还有很多不足的地方,如:
分布式环境下不适用
令牌桶可以有多个,不同的接口采用不同令牌桶的时候,拦截器无法分开限流
一次请求消耗一个令牌,可以被恶意消耗
改进方法:
令牌桶实现采用redis集群存取
注解添加value参数,可以给对应接口打上对应的令牌桶参数,拦截器需对注解参数校验,实现多个接口多个令牌桶的限流
对用户IP校验限制次数,防止恶意攻击
实际项目限流会更加严谨,上述只是提供了一个思路以及演示demo,不喜勿喷谢谢。
版权声明:本文为CSDN博主「Colins~」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
/weixin_44102992/article/details/106492691
公众号“Java精选”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!
------ THE END ------
精品资料,超赞福利!
>Java精选面试题<
3000+ 道面试题在线刷,最新、最全 Java 面试题!
期往精选点击标题可跳转
【221期】扔掉工具类,Mybatis 一个简单配置搞定数据加密解密!
【222期】一个适用于 Spring Boot 项目的轻量级 HTTP 客户端框架
【223期】面试官问:什么是 YAML?和 Spring Boot 有什么关系?
【224期】Java 字符串拼接五种方法的性能比较分析,从执行100次到90万次?
【225期】为什么数据库连接池不采用 IO 多路复用?
【226期】面试问我,HashMap 的默认初始容量是多少,我该怎么说?
【227期】面试官:MySQL 数据查询太多会 OOM 吗?
【228期】面试官:跨库多表存在大量数据依赖问题,有哪些解决方案?
技术交流群!
最近有很多人问,有没有读者&异性交流群,你懂的!想知道如何加入。加入方式很简单,有兴趣的同学,只需要点击下方卡片,回复“加群”,即可免费加入交流群!
文章有帮助的话,在看,转发吧!