前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringBoot - 优雅的实现【流控】

SpringBoot - 优雅的实现【流控】

作者头像
小小工匠
发布2022-03-01 11:44:04
1.3K0
发布2022-03-01 11:44:04
举报
文章被收录于专栏:小工匠聊架构小工匠聊架构

文章目录

概述

限流 简言之就是当请求达到一定的并发数或速率,就对服务进行等待、排队、降级、拒绝服务等操作。

限流算法

我们先简单捋一捋限流算法

并发编程-25 高并发处理手段之消息队列思路 + 应用拆分思路 + 应用限流思路

深入理解分布式技术 - 限流

计数器限流

漏桶算法

把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流


令牌桶算法

可以简单地理解为医去银行办理业务,只有拿到号以后才可以进行业务办理。

系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。

V1.0

上 guava

代码语言:javascript
复制
	  <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>30.1-jre</version>
        </dependency>
代码语言:javascript
复制
package com.artisan.controller;

import com.artisan.annos.ArtisanLimit;
import com.google.common.util.concurrent.RateLimiter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */

@Slf4j
@RestController
@RequestMapping("/rateLimit")
public class RateLimitController {

    /**
     * 限流策略 : 1秒钟1个请求
     */
    private final RateLimiter limiter = RateLimiter.create(1);

    private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @SneakyThrows
    @GetMapping("/test")
    public String testLimiter() {
        //500毫秒内,没拿到令牌,就直接进入服务降级
        boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MILLISECONDS);

        if (!tryAcquire) {
            log.warn("BOOM 服务降级,时间{}", LocalDateTime.now().format(dtf));
            return "系统繁忙,请稍后再试!";
        }

        log.info("获取令牌成功,时间{}", LocalDateTime.now().format(dtf));

        return "业务处理成功";
    }

我们可以看到RateLimiter的2个核心方法:create()、tryAcquire()

  • acquire() 获取一个令牌, 改方法会阻塞直到获取到这一个令牌, 返回值为获取到这个令牌花费的时间
  • acquire(int permits) 获取指定数量的令牌, 该方法也会阻塞, 返回值为获取到这 N 个令牌花费的时间
  • tryAcquire() 判断时候能获取到令牌, 如果不能获取立即返回 false
  • tryAcquire(int permits) 获取指定数量的令牌, 如果不能获取立即返回 false
  • tryAcquire(long timeout, TimeUnit unit) 判断能否在指定时间内获取到令牌, 如果不能获取立即返回 false
  • tryAcquire(int permits, long timeout, TimeUnit unit) 同上

测试一下

V2.0 自定义注解+AOP实现接口限流

1.0的功能实现了,但是业务代码和限流代码混在一起,非常的不美观。

搞依赖

代码语言:javascript
复制
 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

搞自定义限流注解

代码语言:javascript
复制
package com.artisan.annos;

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface ArtisanLimit {

    /**
     * 资源的key,唯一
     * 作用:不同的接口,不同的流量控制
     */
    String key() default "";

    /**
     * 最多的访问限制次数
     */
    double permitsPerSecond();

    /**
     * 获取令牌最大等待时间
     */
    long timeout();

    /**
     * 获取令牌最大等待时间,单位(例:分钟/秒/毫秒) 默认:毫秒
     */
    TimeUnit timeunit() default TimeUnit.MILLISECONDS;

    /**
     * 得不到令牌的提示语
     */
    String message() default "系统繁忙,请稍后再试.";
}

搞AOP

使用AOP切面拦截限流注解

代码语言:javascript
复制
package com.artisan.aop;

import com.artisan.annos.ArtisanLimit;
import com.artisan.resp.ResponseCode;
import com.artisan.resp.ResponseData;
import com.artisan.utils.WebUtils;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */

@Slf4j
@Aspect
@Component
public class ArtisanLimitAop {
    /**
     * 不同的接口,不同的流量控制
     * map的key为 ArtisanLimit.key
     */
    private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap();

    @Around("@annotation(com.artisan.annos.ArtisanLimit)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        //拿ArtisanLimit的注解
        ArtisanLimit limit = method.getAnnotation(ArtisanLimit.class);
        if (limit != null) {
            //key作用:不同的接口,不同的流量控制
            String key = limit.key();
            RateLimiter rateLimiter = null;
            //验证缓存是否有命中key
            if (!limitMap.containsKey(key)) {
                // 创建令牌桶
                rateLimiter = RateLimiter.create(limit.permitsPerSecond());
                limitMap.put(key, rateLimiter);
                log.info("新建了令牌桶={},容量={}", key, limit.permitsPerSecond());
            }
            rateLimiter = limitMap.get(key);
            // 拿令牌
            boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit());
            // 拿不到命令,直接返回异常提示
            if (!acquire) {
                log.warn("令牌桶={},获取令牌失败", key);
                this.responseFail(limit.message());
                return null;
            }
        }
        return joinPoint.proceed();
    }

    /**
     * 直接向前端抛出异常
     *
     * @param msg 提示信息
     */
    private void responseFail(String msg) {
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        ResponseData<Object> resultData = ResponseData.fail(ResponseCode.LIMIT_ERROR.getCode(), msg);
        WebUtils.writeJson(response, resultData);
    }


}

用上验证

代码语言:javascript
复制
   @GetMapping("/test2")
    @ArtisanLimit(key = "testLimit2", permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS, message = "test2 当前排队人数较多,请稍后再试!")
    public String test2() {
        log.info("令牌桶test2获取令牌成功");
        return "test2 ok";
    }

源码

https://github.com/yangshangwei/boot2

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022/02/28 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • 概述
  • 限流算法
    • 计数器限流
      • 漏桶算法
        • 令牌桶算法
        • V1.0
        • V2.0 自定义注解+AOP实现接口限流
          • 搞依赖
            • 搞自定义限流注解
              • 搞AOP
                • 用上验证
                • 源码
                相关产品与服务
                消息队列 CMQ
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档