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. Python绘制折线图

    一.Python绘制折线图 1.1.Python绘制折线图对应代码如下图所示 import matplotlib.pyplot as pltimport numpy as np from pylab ...

  2. Linux 01 计算机硬件之冯诺依曼体系

    1. 计算机硬件软件体系 1.1 冯诺依曼体系结构 (1) 计算机处理的数据和指令用二进制表示 (2) 按顺序执行指令 (3) 计算机硬件:运算器.控制器.储存器.输入设备和输出设备. 1.2 计算机 ...

  3. 逻辑判断(Power Query 之 M 语言)

    逻辑真:true 逻辑假:false 与函数:and true and true,结果为TRUE true and false,结果为FALSE false and false,结果为FALSE 或函 ...

  4. springboot学习(一)

    最近想学习springboot所以在网上找了很多文章参考怎么构建springboot项目以及集成mybatis 集成mybatis的部分参考了这两篇文章 https://blog.csdn.net/t ...

  5. CF169A Chores 题解

    Content 两兄弟要分担 \(n\) 件家务,第 \(i\) 件家务有一个复杂度 \(h_i\).兄弟俩以一个数 \(x\) 为界.所有满足复杂度 \(>x\) 的家务都给哥哥做,其余的给弟 ...

  6. CF710C Magic Odd Square 题解

    Content 构造出一个 \(n\times n\) 的矩阵,使得这个矩阵由 \(1\sim n^2\) 这些数字组成,并且这个矩阵的每行,每列,以及对角线的和都为奇数. 数据范围:\(1\leqs ...

  7. java 输入输出IO流 RandomAccessFile文件的任意文件指针位置地方来读写数据

    RandomAccessFile的介绍: RandomAccessFile是Java输入输出流体系中功能最丰富的文件内容访问类,它提供了众多的方法来访问文件内容,它既可以读取文件内容,也可以向文件输出 ...

  8. [COCI2018-2019#2] Sunčanje 题解

    link 考虑简洁的 \(\text{cdq}\) 做法. 约定:第 \(i\) 个矩形左下角为 \((xl_i,yl_i)\),右上角为 \((xr_i,yr_i)\),所有坐标都已离散化. 如果 ...

  9. 【九度OJ】题目1175:打牌 解题报告

    [九度OJ]题目1175:打牌 解题报告 标签(空格分隔): 九度OJ http://ac.jobdu.com/problem.php?pid=1175 题目描述: 牌只有1到9,手里拿着已经排好序的 ...

  10. 小白自制Linux开发板(第二季 V3s篇) 一. 换个核心再来一次

    1.前言 大家心心念念(个人认为)的小白自制开发板全新系列正式来了,之前我们使用全志的F1C200s芯片制作了一个小电脑,众所周知,调试很艰难,坑也很多,以至于墨云到现在还是没找到对应的补救方案,为了 ...