1. 令牌桶限流算法

令牌桶会以一个恒定的速率向固定容量大小桶中放入令牌,当有浏览来时取走一个或者多个令牌,当发生高并发情况下拿到令牌的执行业务逻辑,没有获取到令牌的就会丢弃获取服务降级处理,提示一个友好的错误信息给用户。

2. RateLimiter简单实现
maven依赖

 
        <!-- guava  -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>

本人使用的是SpringBoot 2.0.4.RELEASE,Jdk1.8环境下编写,部分代码贴出:

存储的最大令牌数maxPermits = maxBurstSeconds * permitsPerSecond;使用RateLimiter.create 创建时,maxBurstSeconds = 1,也就是maxPermits = permitsPerSecond

 
   /**
* 以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依赖

 
        <!-- aop  -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- google guava 依赖 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
<!-- lombok 简化java代码-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

首先定义一个自定义注解:

 
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 Map<String, RateLimiter> limitMap = 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://segmentfault.com/a/1190000016393473

服务限流 -- 自定义注解基于RateLimiter实现接口限流的更多相关文章

  1. SpringBoot使用自定义注解+AOP+Redis实现接口限流

    为什么要限流 系统在设计的时候,我们会有一个系统的预估容量,长时间超过系统能承受的TPS/QPS阈值,系统有可能会被压垮,最终导致整个服务不可用.为了避免这种情况,我们就需要对接口请求进行限流. 所以 ...

  2. spring中实现基于注解实现动态的接口限流防刷

    本文将介绍在spring项目中自定义注解,借助redis实现接口的限流 自定义注解类 import java.lang.annotation.ElementType; import java.lang ...

  3. Guava的RateLimiter实现接口限流

    最近开发需求中有需要对后台接口进行限流处理,整理了一下基本使用方法. 首先添加guava依赖: <dependency> <groupId>com.google.guava&l ...

  4. 一个轻量级的基于RateLimiter的分布式限流实现

    上篇文章(限流算法与Guava RateLimiter解析)对常用的限流算法及Google Guava基于令牌桶算法的实现RateLimiter进行了介绍.RateLimiter通过线程锁控制同步,只 ...

  5. Spring Boot + Redis实战-利用自定义注解+分布式锁实现接口幂等性

    场景 不管是传统行业还是互联网行业,我们都需要保证大部分操作是幂等性的,简单点说,就是无论用户点击多少次,操作多少遍,产生的结果都是一样的,是唯一的.而今次公司的项目里,又被我遇到了这么一个幂等性的问 ...

  6. 基于kubernetes的分布式限流

    做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机 ...

  7. Java 自定义注解与注解解析实例

    在学习Java之后会遇到很多的注解,有加载JavaBean的注解:@Component,@Service,@Controller:有获取配置文件中数值的注解@Value:有获取Http请求的数据的注解 ...

  8. Java中的注解及自定义注解你用的怎么样,能不能像我这样应用自如?

    Java注解提供了关于代码的一些信息,但并不直接作用于它所注解的代码内容.在这个教程当中,我们将学习Java的注解,如何定制注解,注解的使用以及如何通过反射解析注解. Java1.5引入了注解,当前许 ...

  9. Java注解教程及自定义注解

    Java注解提供了关于代码的一些信息,但并不直接作用于它所注解的代码内容.在这个教程当中,我们将学习Java的注解,如何定制注解,注解的使用以及如何通过反射解析注解. Java1.5引入了注解,当前许 ...

随机推荐

  1. numpy基础教程--将二维数组转换为一维数组

    1.导入相应的包,本系列教程所有的np指的都是numpy这个包 1 # coding = utf-8 2 import numpy as np 3 import random 2.将二维数组转换为一维 ...

  2. java多线程6:ReentrantLock

    下面看下JUC包下的一大并发神器ReentrantLock,是一个可重入的互斥锁,具有比synchronized更为强大的功能. ReentrantLock基本用法 先来看一下ReentrantLoc ...

  3. Redis入门及常用命令学习

    Redis简介 Redis 是完全开源的,遵守 BSD 协议,是一个高性能的 key-value 数据库. Redis 与其他 key - value 缓存产品有以下三个特点: Redis支持数据的持 ...

  4. jdk1.8安装教程

    JDK1.8安装包下载 链接:https://pan.baidu.com/s/18pEMo3gYsAAHWC9DjizP1A 提取码:xu99 1.双击JDK1.8的安装包,并点击下一步 2.选择安装 ...

  5. Python3的数据类型

    不可变类型:Number(数值,包含Bool类型).String(字符串).Tuple(元组) 可变类型:List(列表).Dict(字典).Set(集合) Bool不再单独存在,属于Number数值 ...

  6. awk 循环过滤EPC脚本

    无处理中EPC过滤脚本 #!/bin/bash if [ "$#" -lt 3 -o "$#" -gt 4 ];then #if [ "$#" ...

  7. wayne编译支持k8s1.16+

    GitHub: https://github.com/Qihoo360/wayne 文档: 由于wayne 官方文档链接已经失效了,我们可以通过这里查看 wayne 文档, 除了这个地方,我们询问之前 ...

  8. 【LeetCode】1437. 是否所有 1 都至少相隔 k 个元素 Check If All 1s Are at Least Length K Places Away

    作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 指针 日期 题目地址:https://leetcode ...

  9. 【LeetCode】663. Equal Tree Partition 解题报告 (C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 递归 日期 题目地址:https://leetcode ...

  10. 1298 - One Theorem, One Year

    1298 - One Theorem, One Year   PDF (English) Statistics Forum Time Limit: 2 second(s) Memory Limit: ...