Springboot 小巧简便的限流器使用 RateLimiter

news2024/11/17 11:30:26

前言

之前,写过一篇基于redis限流,能应用到分布式相关场景:
(Redis使用系列) Springboot 使用redis实现接口Api限流 十_小目标青年的博客-CSDN博客

也在很久之前,写过一个使用也非常便捷的,整合current-limiting的:

Springboot 整合 Current-Limiting 实现接口限流_小目标青年的博客-CSDN博客

也在很久很久之前,写过一个使用资源数做限流的(可以自己去设计资源令牌的生成等等):

Springboot 线程同步之Semaphore 的简单使用_小目标青年的博客-CSDN博客


那么这次,整个平时敲代码也经常用到的,小巧的限流器,玩的也就是guava的 RateLimiter。

正文

 向技术致敬的最佳方案: 给予技术分享传播者一个点赞、收藏 。

(方案不是很成熟,但是可以尝试)

开搞:
 

① 引入相关依赖,pom.xml  :

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>18.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

② (其实就是使用guava的,简单做了一层业务包装)MyRateLimiter.java :

import com.google.common.util.concurrent.RateLimiter;
import org.springframework.stereotype.Component;


@Component
public class MyRateLimiter {

    /**
     * 账号注册限流器 每秒只发出5个令牌
     */

    private RateLimiter accountRegisterRateLimiter = RateLimiter.create(5.0);

    /**
     * 短信发送限流器    每秒只发出3个令牌
     */

    private RateLimiter smsSendRateLimiter = RateLimiter.create(3.0);

    /**
     * 尝试获取令牌,返回尝试结果
     *
     * @return
     */
    public boolean tryAccountRegisterAcquire() {
        return accountRegisterRateLimiter.tryAcquire();
    }

    /**
     * 取令牌,暂时取不到会一直去尝试
     * @return
     */
    public double accountRegisterAcquire() {
        return accountRegisterRateLimiter.acquire();
    }

    /**
     * 尝试获取令牌
     *
     * @return
     */
    public boolean trySmsSendAcquire() {
        return smsSendRateLimiter.tryAcquire();
    }

}


代码简析:

 

源码解析:


用的简单,但是我们需要简单看看源码,方便我们可以根据业务场景做相关调整。

 

简单翻译一下两个用的比较多的create函数里面的注释:

方法 一

public static RateLimiter create(double permitsPerSecon);

咱大白话翻译(其实源码上有注释,还有举例):

保证每秒处理不超过 permitsPerSecond个请求。
如果每秒请求数爆炸,超过我们设置的permitsPerSecond 数量,会慢慢处理。
如果每秒请求书很少,这个permitsPerSecond相当于令牌,会囤积起来,最多囤积permitsPerSecond个。

方法 二

public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) ;

咱大白话翻译(其实源码上有注释,还有举例):

保证了平均每秒不超过permitsPerSecond个请求。
但是这个创建出来的限流器有一个热身期(warmup period)。
热身期内,RateLimiter会平滑的将其释放令牌的速率加大,直到起达到最大速率。
同样,如果RateLimiter在热身期没有足够的请求(unused),则起速率会逐渐降低到冷却状态。

玩过阿里的那个 Sentinel组件的话,应该对这种热身限流策略不会陌生,其实限流策略都是这几种,万变不离其宗。

设计这个的意图是为了满足那种资源提供方需要热身时间,
而不是每次访问都能提供稳定速率的服务的情况(比如带缓存服务,需要定期刷新缓存的)
参数warmupPeriod和unit决定了其从冷却状态到达最大速率的时间。

再来简单看看 尝试获取令牌的 tryAcquire函数

//尝试获取一个令牌,立即返回尝试结果

public boolean tryAcquire();

//尝试获取 permits 个令牌,立即返回尝试结果
public boolean tryAcquire(int permits);

//尝试获取一个令牌,带超时时间传参
public boolean tryAcquire(long timeout, TimeUnit unit);

//尝试获取permits个令牌,带超时时间传参
public boolean tryAcquire(int permits, long timeout, TimeUnit unit);

 

再看看 获取令牌的 acquire函数

//默认 permits是 1 ,也就是默认拿1个令牌
public double acquire();
//令牌自己定,权重大一点的业务,也许需要拿3个令牌才能执行一次(举例)
public double acquire(int permits); 

 

可以看到返回值是个dubbo ,其实这是一个等待的时间:

rateLimiter.acquire()该方法会阻塞线程,直到令牌桶中能取到令牌为止才继续向下执行,并返回等待的时间。

好了,开始结合实际案例玩一把 。

模拟场景,我们提供一个 账号注册接口,注册接口需要限流。

然后我们再模拟一个并发接口,多线程去调度 注册接口,模拟出 注册接口被短时间并发调用的场景,看看限流器RateLimiter 玩出来的效果。

首先写个简单的HTTP GET 请求调用函数:
 

HttpUtil.java

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;

public class HttpUtil {

    /**
     * get请求
     *
     * @param realUrl
     * @return
     */
    public static String sendGet(URL realUrl) {
        String result = "";
        BufferedReader in = null;
        try {
            // 打开和URL之间的连接
            URLConnection connection = realUrl.openConnection();
            // 设置通用的请求属性
            connection.setRequestProperty("accept", "*/*");
            connection.setRequestProperty("connection", "Keep-Alive");
            connection.setRequestProperty("user-agent",
                    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            // 建立实际的连接
            connection.connect();
            // 定义 BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(
                    connection.getInputStream()));
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        } catch (Exception e) {
            System.out.println("发送GET请求出现异常!" + e);
            e.printStackTrace();
        }
        // 使用finally块来关闭输入流
        finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        return result;
    }


}

然后写个模拟的注册接口:
 

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Controller
public class UserController {

    @Autowired
    MyRateLimiter myRateLimiter;

    @RequestMapping("/userRegister")
    @ResponseBody
    public String userRegister() {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
        //尝试获取令牌
        boolean acquire = myRateLimiter.tryAccountRegisterAcquire();
        System.out.println(Thread.currentThread().getName() + " 尝试 获取令牌结果"+acquire);
        if (acquire) {
            try {
                //模拟业务执行500毫秒
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return Thread.currentThread().getName()+"userRegister 拿到令牌很顺利success [" +  dtf.format(LocalDateTime.now()) + "]";
        } else {
            return Thread.currentThread().getName()+"userRegister 被 limit 限制了 [" +  dtf.format(LocalDateTime.now())+ "]";
        }
    }
}

可以看到这个接口里面,我们当前只 玩了一下 尝试获取令牌函数 tryAcquire   

OK,我们来 写个多线程调用接口,来看看这时候限流的效果:

 

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import static com.example.dotest.util.HttpUtil.sendGet;
import static java.util.concurrent.Executors.*;

@RestController
public class TestController {
    ExecutorService fixedThreadPool = newFixedThreadPool(10);
    @RequestMapping("/test")
    public void test() throws MalformedURLException, InterruptedException {
        final URL url = new URL("http://localhost:8696/userRegister");
        for(int i=0;i<10;i++) {
            fixedThreadPool.submit(() -> System.out.println(sendGet(url)));
        }
        fixedThreadPool.shutdown();
        fixedThreadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
    }
}

简析:

 可以看到效果:

 

可以看到我们的代码,目前用的这个 tryAcquire 函数 ,返回boolean值 ,只要是拿不到令牌我们就直接不做处理了。

这时候其实也可以考虑这么使用,拿不到的 做一些降级、熔断操作或者重试等待啥的。

那么,我们如果想,尝试拿的时候拿不到,让线程自己去帮我们继续自旋去等待持续获取令牌呢?

这时候我们就需要用的是   acquire  函数   

改造一下刚才的模拟接口:

 

    @RequestMapping("/userRegister")
    @ResponseBody
    public String userRegister() {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
        //尝试获取令牌
        boolean tryAcquire = myRateLimiter.tryAccountRegisterAcquire();
        System.out.println(Thread.currentThread().getName() + " 尝试 获取令牌结果"+tryAcquire);
        double registerAcquireWaitTime = myRateLimiter.accountRegisterAcquire();
        System.out.println(Thread.currentThread().getName() + "  坚持  获取令牌,被限制的时间是"+registerAcquireWaitTime);
        if (tryAcquire) {
            //模拟业务执行500毫秒
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return Thread.currentThread().getName()+"userRegister 拿到令牌很顺利success [" +  dtf.format(LocalDateTime.now()) + "]";
        } else {
            return Thread.currentThread().getName()+"userRegister拿到令牌不是很顺利被 limit 过,但是还是拿到了 [" +  dtf.format(LocalDateTime.now())+ "]";
        }
    }
}

简析: 

 

继续调用一下接口,看看这时候限流器的效果:

限流效果: 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/22637.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Google Earth 成长历程的15个小故事

利用 Google Earth&#xff0c;可以像宇航员一样从太空中看到我们的星球&#xff0c;只需点击或轻触几秒钟就可以在地球上的任何地方旅行。如今的 Google Earth 仍然是世界上最大的可公开获取的地理图像存储库。它将航空摄影、卫星图像、3D 地形、地理数据和街景组合成一幅可以…

web前端面试-10大经典题(HTML基础)

HTML基础 1. HTML 文件中的 DOCTYPE 是什么作用&#xff1f; HTML超文本标记语言: 是一个标记语言, 就有对应的语法标准 DOCTYPE 即 Document Type&#xff0c;网页文件的文档类型标准。 主要作用是告诉浏览器的解析器要使用哪种 HTML****规范 或 XHTML****规范 来解析页面…

项目部署与拉取Github/Gitlab/Gitee的合理步骤以及会遇到的问题

踩了很多坑&#xff0c;总结一下。首先有两种需求&#xff0c;第一种是本地的项目部署到Github上&#xff0c;第二种是将团队的项目拉到本地。 &#xff08;初始&#xff09;本地 -> GitHub 因为本地到Github有可能是第一次去推送代码&#xff0c;也有可能是你更改了拉下来…

12V铅酸电池充放电保护板

现有铅酸电池特性&#xff1a; 重量&#xff1a;3斤电压范围&#xff1a;13.5~13.8V 14.4V~14.7V最大输出电流&#xff1a;2.16A 选用芯片&#xff1a;CN3768&#xff0c;4A&#xff0c;12V铅酸电池充放电管理集成电路 概述 CN3768是PWM降压模式12V铅酸电池充电管理集成…

关于python函数,你该了解这些

目录 1.创建一个函数 举例 2.调用函数 形参 实参 位置参数 关键字参数 可变长参数 其他 变量的作用域 全局变量 局部变量 3.匿名函数 1.创建一个函数 语法格式 def functionname([parameterlist]):[functionbody] functionname:函数名称&#xff0c;在调用函数时…

[附源码]java毕业设计闲置物品线上交易系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

web课程设计使用html+css+javascript+jquery技术制作个人介绍6页

&#x1f389;精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

追踪这9大关键DTC指标,将帮助你建立势头并释放增长

由于新冠疫情与检疫要求迫使品牌在分销产品的方式上进行创新&#xff0c;许多品牌转向了DTC模式。现在&#xff0c;DTC模式的吸引力越来越强&#xff0c;竞争也越来越激烈。在激烈的竞争中&#xff0c;犯一点小错误都可能贻害无穷。企业必须站在市场最前端&#xff0c;并专注自…

ts泛型,映射,条件类型和类型提取infer和一些常用工具库的说明

Typescript当中的T,K,V到底是个啥 有时候,我们看到下面的代码,当然,这里是简单例子来说 function identity <T> (value:T) : T {return value; }其实泛型就是使用字母来代替将要接收的类型,这里的"T"是代表类型的缩写,表示对将要接收类型的一个占位符,占位符…

【k8s】4、资源管理命令-陈述式

文章目录一、资源管理介绍1、资源管理概念1、资源管理方式二、 陈述式对象管理1、基本概念2、基础命令使用3、基本信息查看&#xff08;kubectl get&#xff09;4、增删等操作5、登录pod中的容器6、扩容缩容pod控制器的pod7、标签操作&#xff08;labels&#xff09;8、简单案例…

5G无线技术基础自学系列 | 5G信道结构

素材来源&#xff1a;《5G无线网络规划与优化》 一边学习一边整理内容&#xff0c;并与大家分享&#xff0c;侵权即删&#xff0c;谢谢支持&#xff01; 附上汇总贴&#xff1a;5G无线技术基础自学系列 | 汇总_COCOgsta的博客-CSDN博客 5G的信道包括逻辑信道、传输信道、物理…

jdk线程池ThreadPoolExecutor优雅停止原理解析(自己动手实现线程池)(二)

ThreadPoolExecutor优雅停止源码分析(自己动手实现线程池v2版本) ThreadPoolExecutor为了实现优雅停止功能&#xff0c;为线程池设置了一个状态属性&#xff0c;其共有5种情况。 在第一篇博客中曾介绍过&#xff0c;AtomicInteger类型的变量ctl同时维护了两个业务属性当前活跃…

SpringBoot SpringBoot 开发实用篇 5 整合第三方技术 5.8 变更缓存供应商 memcached

SpringBoot 【黑马程序员SpringBoot2全套视频教程&#xff0c;springboot零基础到项目实战&#xff08;spring boot2完整版&#xff09;】 SpringBoot 开发实用篇 文章目录SpringBootSpringBoot 开发实用篇5 整合第三方技术5.8 变更缓存供应商 memcached5.8.1 memcached 缓存…

如何在 Navicat 16 中仅备份数据库结构 | 数据传输

尽管有少数据库管理员&#xff08;DBA&#xff09;不相信执行定期数据库备份是有用的&#xff0c;但对于如何最好地执行此操作有很多意见。无论你采用哪种方法&#xff0c;都有很多充分的理由保留数据库模式的副本。当发生数据丢失时&#xff0c;你可以从模式中还原数据库结构&…

[附源码]SSM计算机毕业设计二手车交易系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Java中的final和常量以及枚举

Java中的final和常量以及枚举final的作用常量常量概述和基本作用常量做信息标志和分类枚举枚举的概念定义枚举类的格式枚举的特征final的作用 1.final关键字是最终的意思&#xff0c;可以修饰&#xff08;类、方法、变量&#xff09; 2.修饰类&#xff1a;表明该类是最终类&am…

数据结构之:链表

链表初体验之单链表 线性表 线性表"线性存储结构" —— 一根线能串起来的数组 存储到物理空间之中 数据需要有相同的数据类型 元素之间的关系 需要是“一对一” 两种存储方式“顺序” 和“链式”链表介绍 分为有头节点的链表和没有头节点的链表。 插入的时候&#xf…

化合物在高通量筛选中的作用

在 1985 年之前&#xff0c;先导物的筛选主要是通过人工进行的&#xff0c;每周处理的样本数量不过几百个&#xff0c;组合化学的出现使得科学家们获取化合物的方式发生了显著变化&#xff0c;他们可以在短时间内合成大量化合物。更重要的是&#xff0c;随着分子生物学和功能基…

【构建ML驱动的应用程序】第 1 章 :从产品目标到 ML 框架

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

SpringBoot框架详细教学保姆级说明

目录 文章目录1.1 简介1.2 特性1.3 四大核心2 springboot入门案例2.1 SpringBoot 项目开发步骤2.2 创建一个 Spring MVC 的 Spring BootController2.3 分析2.4 核心配置文件格式2.5 Spring Boot 前端使用 JSP3 SpringBoot框架Web开发3.1 Spring Boot 集成 MyBatis3.2 DAO 的其它…