【开发经验】java代码中实现限流
限流目的
限流的目的是防止恶意请求流量、恶意攻击、或者防止流量超过系统峰值。
流量达到峰值时,会有一个熔断策略,常见的熔断策略:
- 直接拒绝请求,跳转到一个“服务器出小差”页面
- 排队等待,比如火车票等待。
- 服务降级,返回最基本的用户可接受的数值。
- 到达峰值的时候进行排队。
- 定位经纬度定位,精确定位到达峰值,返回大概定位,比如百度地图,有时候可以定位到自己的详细位置,有时候显示自己所在的城市。
限流实现的方式有很多种,比如nginx限流,网关限流,或者在代码中限流。具体如何限流,也要根据不同的场景进行选择。以下介绍下如何在代码中进行限流。
限流方式
漏桶
漏桶是有一个进水口,一个出水口。桶本身具有一个恒定的速率往下漏水,而上方时快时慢的会有水进入桶内。 进水速度可以大于出水速度,但是多余的水会积在桶中,一旦水满,上方的水就无法加入。
特点:漏水速度固定。
令牌桶
令牌桶是一个存放固定容量令牌(token)的桶,按照固定速率往桶里添加令牌。当请求到达当时,从桶中拿令牌,有令牌即可进行请求。
限流实现
单机限流
1.Semaphore限流
Semaphore是jdk自带的一种并发限流方式。通过new Semaphore(2);
可设置并发数为2。切记获取资源后,要进行资源释放。
public static void main(String[] args) {//并发为2个 Semaphore semaphore = new Semaphore(2);Random random = new Random();for(int i = 0 ; i<5;i++){new Thread(()->{try {semaphore.acquire();System.out.println(Thread.currentThread().getName()+"获取权限");Thread.sleep(random.nextInt(2000)); // 仿造处理时间} catch (InterruptedException e) {e.printStackTrace();}finally {semaphore.release(); //注意一定要finally中进行释放资源System.out.println(Thread.currentThread().getName()+"释放");}}).start();}}
- Semaphore.acquire/release方法要配对使用,使用acquire申请资源,使用release释放资源。Semaphore即使在没有申请到资源的情况下,还是可以通过release释放资源,这里就要自己通过代码进行合适的处理。和锁不同的是,锁必须获得锁才能释放锁,这里并没有这种限制。
- Semaphore.release方法尽可能的放到finally中避免资源无法归还。
- 如果Semaphore的配额为1,那么创建的实例相当与一个互斥锁,由于可以在没有申请资源的情况调用release释放资源,所以,这里允许一个线程释放另一个线程的锁。
2.RateLimiter限流
Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库,例如:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等等。 所有这些工具每天都在被Google的工程师应用在产品服务中。更多信息。
使用Guava的RateLimiter类通过令牌桶进行限流。
引入jar
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>28.1-jre</version></dependency>
1普通令牌桶
public static void main(String[] args) {RateLimiter r = RateLimiter.create(5);for(;;){System.out.println("get 1 tokens: " + r.acquire() + "s");}/*** output: 基本上都是0.2s执行一次,符合一秒发放5个令牌的设定。* get 1 tokens: 0.0s* get 1 tokens: 0.182014s* get 1 tokens: 0.188464s* get 1 tokens: 0.198072s* get 1 tokens: 0.196048s* get 1 tokens: 0.197538s* get 1 tokens: 0.196049s*/}
通过RateLimiter.create(5);方法声明每秒创建5个令牌。
acquire
方法进行获取令牌,并且返回获取时间。
令牌积累情况
public static void main(String[] args) throws InterruptedException {RateLimiter r = RateLimiter.create(2);for(;;){System.out.println("get 1 tokens: " + r.acquire() + "s");System.out.println("get 1 tokens: " + r.acquire() + "s");Thread.sleep(2000l);System.out.println("get 1 tokens: " + r.acquire() + "s");System.out.println("get 1 tokens: " + r.acquire() + "s");System.out.println("get 1 tokens: " + r.acquire() + "s");System.out.println("get 1 tokens: " + r.acquire() + "s");System.out.println("get 1 tokens: " + r.acquire() + "s");}/*** get 1 tokens: 0.0s* get 1 tokens: 0.49874s* 休眠2 秒* get 1 tokens: 0.0s* get 1 tokens: 0.0s // 积累了两个* get 1 tokens: 0.0s // 预支1个,预支是什么?下面说* get 1 tokens: 0.499827s* get 1 tokens: 0.499159s* get 1 tokens: 0.49749s* get 1 tokens: 0.499572s*/}
在没有足够令牌发放时,采用滞后处理的方式,也就是前一个请求获取令牌所需等待的时间由下一次请求来承受,也就是代替前一个请求进行等待。
public static void main(String[] args) throws InterruptedException {RateLimiter r = RateLimiter.create(2);System.out.println("get 1 tokens: " + r.acquire(4) + "s");System.out.println("get 1 tokens: " + r.acquire() + "s");System.out.println("get 1 tokens: " + r.acquire() + "s");System.out.println("get 1 tokens: " + r.acquire() + "s");/**get 1 tokens: 0.0s 第一次获取4个get 1 tokens: 1.998205s ,第二次获取要为第一次买单,所以等待两秒,如果第一次获取6个,则就是等待3秒。get 1 tokens: 0.496947sget 1 tokens: 0.499579s*/}
2.预热令牌桶
RateLimiter.create(2, 3, TimeUnit.SECONDS);
说明需要3秒的预热时间,开始的时候是达不到每秒中2个令牌,在3秒内创建令牌速度逐渐加快,最后达到每秒钟2个令牌。
预热令牌桶场景:可能有缓存失效的时候,这段时间需要和数据库交互,进行一个缓存的预热。
public static void main(String[] args) {RateLimiter r = RateLimiter.create(2, 3, TimeUnit.SECONDS);for (;;){System.out.println("get 1 tokens: " + r.acquire(1) + "s");System.out.println("get 1 tokens: " + r.acquire(1) + "s");System.out.println("get 1 tokens: " + r.acquire(1) + "s");System.out.println("get 1 tokens: " + r.acquire(1) + "s");System.out.println("end");/*** output:* get 1 tokens: 0.0s* get 1 tokens: 1.329289s* get 1 tokens: 0.994375s* get 1 tokens: 0.662888s 上边三次获取的时间相加正好为3秒* end* get 1 tokens: 0.49764s 正常速率0.5秒一个令牌* get 1 tokens: 0.497828s* get 1 tokens: 0.49449s* get 1 tokens: 0.497522s*/}
}
参考资料:超详细的Guava RateLimiter限流原理解析
集群限流
redis限流
在正式的开发过程中,往往是集群化部署的,那么上面的单机限流就力不从心了。可以采用redis进行集群限流。redission框架通过lua脚本实现的集群限流。
redission通过getRateLimiter方法进行限流,使用方式和guava的限流类似。
public static void main(String[] args) {Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");RedissonClient client = Redisson.create(config);RRateLimiter rateLimiter = client.getRateLimiter("order"); RateType.OVERALL, //所有客户端加总限流,就是集群下所有的流量 RateType.PER_CLIENT; //每个客户端单独计算流量,每台机器的流量rateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS);ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {executorService.submit(() -> {try {long start = System.currentTimeMillis();rateLimiter.acquire();System.out.println("线程" + Thread.currentThread().getName() + "进入数据区:" + (start-System.currentTimeMillis()));} catch (Exception e) {e.printStackTrace();}});}/*** 线程pool-1-thread-4进入数据区:-10* 线程pool-1-thread-5进入数据区:-13* 线程pool-1-thread-7进入数据区:-1010* 线程pool-1-thread-10进入数据区:-1013* 线程pool-1-thread-1进入数据区:-2017* 线程pool-1-thread-9进入数据区:-2016* 线程pool-1-thread-8进入数据区:-3021* 线程pool-1-thread-3进入数据区:-3022* 线程pool-1-thread-2进入数据区:-4028* 线程pool-1-thread-6进入数据区:-4028*/}
Sentinel限流
Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
- 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel官方文档
【开发经验】java代码中实现限流相关推荐
- 高并发中的 限流、熔断、降级、预热、背压你都知道是什么意思吗?
首先,我们需要明确一下这几个名词出现的场景:分布式高并发环境.如果你的产品卖相不好,没人鸟它,那它就用不着这几个属性.不需要任何加成,低并发系统就能工作的很好. 分布式系统是一个整体,调用关系错综复杂 ...
- 如何在java代码中读取配置文件
在日常开发过程中,我们经常需要拼接一些字符串之类的东西,而这些字符串往往是不变的,或者在java代码中多次使用到的.当然我们可以在java代码中写死,但是这样做的缺点也是有目共睹的,一旦业务需求发生变 ...
- 你还在 Java 代码中写 set/get 方法?赶快试试这款插件吧!
点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:Mr.ml https://blog.csdn.net/Ma ...
- java代码中fastjson生成字符串和解析字符串的方法和javascript文件中字符串和json数组之间的转换方法...
1.java代码中fastjson生成字符串和解析字符串的方法 List<TemplateFull> templateFulls = new ArrayList<TemplateFu ...
- idea自动生成get set_CTO:不要在Java代码中写set/get方法了,逮一次罚款
前言 what?你的 Java 代码中还充斥着大量的 set/get 方法? 我们在刚开始学习 Java 语言的时候讲过,面向对象的三大特征就是封装,继承,和多态.在 Java 中,要保证封装性,需要 ...
- Android如何在java代码中设置margin
Android如何在java代码中设置margin,也就是组件与组件之间的间距. 代码中设置: LinearLayout.LayoutParams params = new LinearLayout. ...
- 如何在android的XML和java代码中引用字符串常量
使用字符串(string)资源 在一个Android工程中,我们可能会使用到大量的字符串作为提示信息.这些字符串都可以作为字符串资源声明在配置文件中,从而实现程序的可配置性. 在代码中我 ...
- java : enum、创建文件和文件夹、删除文件和文件夹、获得项目绝对路径、写入数据到excel中、java代码中两种路径符号写法、读取、写入text文件...
java : enum http://www.cnblogs.com/hyl8218/p/5088287.html 创建文件和文件夹.删除文件和文件夹 http://www.cnblogs.com/m ...
- java中getup用法_你还在 Java 代码中写 set/get 方法?赶快试试这款插件吧!
前言 what?你的 Java 代码中还充斥着大量的 set/get 方法? 我们在刚开始学习 Java 语言的时候讲过,面向对象的三大特征就是封装,继承,和多态.在 Java 中,要保证封装性,需要 ...
最新文章
- 格式工厂mac版_格式工厂无广告版,支持PDF文件的转换
- ML机器学习导论学习笔记
- vba fso读utf 文本_利用FSO对象操作文件
- Windows 2008活动目录的安装和卸载
- python怎么索引txt数据中第四行_python txt读取第一行数据库
- fir滤波器matlab实现_关于FIRamp;IIR系统的算法说明以及结果验证(1)
- python写web自动化_jenkins+selenium+python实现web自动化测试
- 老鸟对菜鸟的一些建议
- 遗传相似系数怎么计算_如何计算遗传变异系数
- hdfs读写流程_一篇文章搞清楚 HDFS 基本原理
- Shopee Games 游戏引擎演进之路
- Slickedit 打开Qt工程
- 我用 Python 自制成语接龙小游戏,刺激!
- 迅雷版权限制无法下载破解
- CF802C Heidi and Library (hard) (网络流+最大流)
- 《实用软件工程答案》张海涛人民邮电出版社
- CGAL例程:地理信息系统----点云数据生成DSM、DTM、等高线和数据分类
- 基于ESP32+AMG8833的物联网红外成像测温枪
- 技术分享:应用于厚型气体电子倍增器的高耐压PCB研究
- 韩信点兵 中国剩余定理