⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/weixin_44102992/article/details/106492691 「Colins~」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

令牌桶

在高并发的情况下,限流是后端常用的手段之一,可以对系统限流、接口限流、用户限流等,本文就使用令牌桶算法+拦截器+自定义注解+自定义异常实现限流的demo。

令牌桶思想

大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。

后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。然后每个访问的用户都会从中取走一块令牌,取到了令牌才能访问,如果没取到令牌即代表已达到访问上限,将被限流不允许访问

限流demo实现思路

  • 创建令牌桶类
  • 项目启动初始化令牌桶,并设置定时器,定时向桶内放入令牌
  • 自定义限流注解,在需要限流的接口上打上注解
  • 配置令牌桶拦截器,对所有路径进行拦截,对无限流注解的接口直接放行,对有限流注解的做取令牌处理,取到令牌即放行,没取到令牌即抛出自定义异常
  • 自定义异常并使用AOP做全局异常处理

创建令牌类

这里为了防止并发问题在生成令牌和取令牌的方法上加了synchronized

BucketUtil如下

public class BucketUtil {

//默认容量10
static final int DEFAULT_MAX_COUNT = 10;
// 默认增长速率为1
static final int DEFAULT_CREATE_RATE = 1;
// 使用HashMap存放令牌桶,这里默认为10个令牌桶
public static HashMap<String, BucketUtil> buckets = new HashMap(10);

//自定义容量,一旦创建不可改变
final int maxCount;
//自定义增长速率1s几个令牌
int createRate;
//当前令牌数
int size=0;



// 默认令牌桶的容量及增长速率
public BucketUtil() {
maxCount = DEFAULT_MAX_COUNT;
createRate = DEFAULT_CREATE_RATE;
}
// 自定义令牌桶容量及增长速率
public BucketUtil(int maxCount, int createRate) {
this.maxCount = maxCount;
this.createRate = createRate;
}

public int getSize() {
return size;
}

public boolean isFull() {
return size == maxCount;
}

//根据速率自增生成一个令牌
public synchronized void incrTokens() {
for (int i = 0; i < createRate; i++)
{
if (isFull())
return;
size++;
}
}

// 取一个令牌
public synchronized boolean getToken() {
if (size > 0)
size--;
else
return false;
return true;
}

@Override
public boolean equals(Object obj) {
if (obj == null)
return false;
BucketUtil bucket = (BucketUtil) obj;
if (bucket.size != size || bucket.createRate != createRate || bucket.maxCount != maxCount)
return false;
return true;
}

@Override
public int hashCode() {
return Objects.hash(maxCount, size, createRate);
}

}

初始化令牌桶

在启动类上初始化并生成定时器

@EnableScheduling
@SpringBootApplication
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
// 为了方便测试这里定义1容量 1增长速率
BucketUtil bucketUtil = new BucketUtil(1,1);
// 生成名为:bucket的令牌桶
BucketUtil.buckets.put("bucket",bucketUtil);
}
@Scheduled(fixedRate = 1000)// 定时1s
public void timer() {
if (BucketUtil.buckets.containsKey("bucket")){
//名为:bucket的令牌桶 开始不断生成令牌
BucketUtil.buckets.get("bucket").incrTokens();
}
}
}

自定义注解以及异常

BucketAnnotation注解

@Target({ElementType.METHOD})// METHOD代表是用在方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface BucketAnnotation {
}

APIException 异常

public class APIException extends RuntimeException {
private static final long serialVersionUID = 1L;
private String msg;
public APIException(String msg) {
super(msg);
this.msg = msg;
}
}

配置拦截器

BucketInterceptor拦截器如下

/**
* 令牌桶拦截器
*/
public class BucketInterceptor implements HandlerInterceptor {

// 预处理回调方法,在接口调用之前使用 true代表放行 false代表不放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (!(handler instanceof HandlerMethod)) {
return true;
}

HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();

BucketAnnotation methodAnnotation = method.getAnnotation(BucketAnnotation.class);
if (methodAnnotation!=null){
// 在名为:bucket的令牌桶里取令牌 取到即放行 未取到即抛出异常
if(BucketUtil.buckets.get("bucket").getToken()){
return true;
}
else{
// 抛出自定义异常
throw new APIException("不好意思,您被限流了");
}
}else {
return true;
}
}
// 接口调用之后,返回之前 使用
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}

// 整个请求完成后,在视图渲染前使用
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}

将拦截器注入

@Configuration
public class WebMvcConfg implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
// 令牌桶拦截器 添加拦截器并选择拦截路径
registry.addInterceptor(bucketInterceptor()).addPathPatterns("/**");
}
@Bean
public BucketInterceptor bucketInterceptor() {
return new BucketInterceptor();
}
}

AOP全局异常处理

@RestControllerAdvice
public class WebExceptionControl {
@ExceptionHandler(APIException.class)
public E3Result APIExceptionHandler(APIException e) {
return E3Result.build(400,e.getMessage());
}
}

测试

在我们需要限流的接口上打上自定义注解,如下

@BucketAnnotation
@RequestMapping(value = "/bucket")
public E3Result bucket(){
return E3Result.ok("访问成功");
}

关于E3Result只是一个封装好的返回类,这里就不贴出来了,大家有的替换成自己的,没有的可以直接用String型测试

上面为了方便测试,令牌桶的容量设置成了1,所以这是取到令牌成功的

这是没取到令牌被限流的

总结

上面的限流只是一个demo还有很多不足的地方,如:

  • 分布式环境下不适用
  • 令牌桶可以有多个,不同的接口采用不同令牌桶的时候,拦截器无法分开限流
  • 一次请求消耗一个令牌,可以被恶意消耗

改进方法:

  • 令牌桶实现采用redis集群存取
  • 注解添加value参数,可以给对应接口打上对应的令牌桶参数,拦截器需对注解参数校验,实现多个接口多个令牌桶的限流
  • 对用户IP校验限制次数,防止恶意攻击

实际项目限流会更加严谨,上述只是提供了一个思路以及演示demo,不喜勿喷谢谢。

文章目录
  1. 1. 令牌桶
    1. 1.0.1. 限流demo实现思路
  • 2. 创建令牌类
  • 3. 初始化令牌桶
  • 4. 自定义注解以及异常
  • 5. 配置拦截器
  • 6. AOP全局异常处理
  • 7. 测试
  • 8. 总结