API限速

最近遇到几个很有意思的接口,这些接口具有调用速率的限制,比如说一个接口具有每小时请求30次的限制,就是一小时只能请求这个接口30次,如果超过了30

次,那么接口服务方会启用惩罚策略,将调用的速率减小一些,比如说每小时1次请求,等限制期的时间过去后,才恢复正常的请求限制率。

 

对于接口限制策略,必须有一个请求采样算法,我猜测了它的内部实现,有可能是这样的。对于每小时30次的请求限制策略,构建一个具有30个元素大小的先进先

出的请求队列,这个队列放的就是每次的请求时间,当一个请求来临时,其请求时间是t_new,查看队列的尾部的元素,会获得相较于本次请求之前的第30次请求的

请求时间t_last,我们把t_new减去t_last,获得的值,便是30次调用之间的时间值,这个差值如果大于1小时,那便是在允许的1小时30次请求的范围之内的,如果小

于1小时,那么就是超出1个小时调用30次请求的范围的,接下来就采取惩罚策略,这个惩罚策略,我们就不细讨论了。可以用一张图来说明原理。

 

 

对于这样的请求次数限制策略,在本地使用Java代码来模拟一下实现这个算法,这里是1秒内限制30次调用,如下代码所示:

 

public class LimitedCalle { private long baseBetweenTime = 1*1000; private int acceptCallCount = 30; private ArrayDeque<Date> arrayDeque = new ArrayDeque<>(); public void call(){ int callCount = this.arrayDeque.size(); Date now = new Date(); if(callCount <= this.acceptCallCount){ this.arrayDeque.addFirst(now); this._call(); }else{ Date last = this.arrayDeque.getLast(); long lastTime = last.getTime(); long nowTime = now.getTime(); long betweenTime = nowTime - lastTime; if(betweenTime < baseBetweenTime){ throw new RuntimeException("对不起,您调用我实在是太快了,我接不住了。"); }else{ this.arrayDeque.addFirst(now); this._call(); this.arrayDeque.removeLast(); } } } private void _call(){ System.out.println("被调用!!!"); }}

 

写一段代码来测试一下段代码,如下所示

 

public static void main(String[] args) { LimitedCalle calle = new LimitedCalle(); for(int i=0;i<100;i++){ System.out.println("第"+i+"次..."+System.currentTimeMillis()); calle.call(); }}

 

 

我们来看一下结果:

 

针对这样的限速策略,我们只有在调用的时候,统计调用次数,记录下第一次调用的时间t1,当开始第31次调用开始时,获得当前时间t2,t2 - t1就是30次调用

之间的时间值,1000减去这个值,就是我们需要等待的时间,将这1秒内的时间全部用完后,然后在继续第31次调用,根据上面的想法,写了一个这样的类。

public abstract class AbstractTimeMethodCallerCountLimiter { private long limitTime; private int acceptCallCount; private long watiExtendTime = 100; private int callCount; private Date startReRunTime = null; public AbstractTimeMethodCallerCountLimiter(final long limitTime,final int acceptCallCount) { super(); this.limitTime = limitTime; this.acceptCallCount = acceptCallCount; this.callCount = 0; } public void run() { if(startReRunTime == null){ startReRunTime = new Date(); } int callCount = this.callCount; if (callCount < this.acceptCallCount) { this.doRun(); } else { Date now = new Date(); long nowTime = now.getTime(); Date firstRunTimeDate = this.startReRunTime; long firstRunTime = firstRunTimeDate.getTime(); long runTime = nowTime - firstRunTime; long waitTime = this.limitTime - runTime; synchronized (this) { System.out.println("等待时间:"+waitTime); try { this.wait(waitTime + this.watiExtendTime); } catch (InterruptedException e) { e.printStackTrace(); } } this.startReRunTime = new Date(); this.callCount = 0; this.doRun(); } } protected abstract void doRun(); }

 

 

这里面有一个waitExtendTime,设置了100毫秒的时间值,多等待100毫秒,这个是个抽象的类,需要我们自定义子类来实现我们具体的调用逻辑,如下,我自己定义了一个:

public class DemoCaller extends AbstractTimeMethodCallerCountLimiter{ private LimitedCalle calle; public DemoCaller(long limitTime, int acceptCallCount,LimitedCalle calle) { super(limitTime, acceptCallCount); this.calle = calle; } @Override protected void doRun() { System.out.println("我调用你"); this.calle.call(); }}

 

来一段测试代码和测试结果

 

public static void main(String[] args) { LimitedCalle calle = new LimitedCalle(); DemoCaller demoCaller = new DemoCaller(1*1000, 30,calle); for(int i=0;i<100;i++){ System.out.println("i:"+i); demoCaller.run(); } }

 

 

 

 

这样大约就已经完成了对接这个接口的功能了,但是,这几个接口的查询接口有些特殊,我来用Java代码模拟一下

 

public interface Request { public Response request(RequestParam param); public Response requestByNextToken(String nextToken,RequestParam param);}如同上面的代码,第一次查询,我们可以使用第一个请求方式request方法,但是,如果存在后续查询结果,就必须使用第二个请求方式,调用requestByNextToken了。而且,我们上面的使用模板设计模式的代码,明显不支持这样的功能了。这样的查询,第二次查询依赖第一次的查询结果,第三次的查询依赖第二次的查询结果,所以具有上下文关系。

 

 

由上下文关系,我想到了在使用Netty框架对接一个TCP网络接口时,一个非常经典的操作,可以说非常经典。相关类似的代码如下:

 

public class DemoHandler extends ChannelInboundHandlerAdapter{ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.executor().schedule(new DemoTask(ctx), 10, TimeUnit.SECONDS); } private static class DemoTask implements Runnable{ private ChannelHandlerContext ctx; public DemoTask(ChannelHandlerContext ctx) { super(); this.ctx = ctx; } @Override public void run() { ctx.executor().schedule(this, 10, TimeUnit.SECONDS); } } }这里,将通道的上下文信息封装在ChannelHandlerContext对象里面,这个对象可以调度任务,在新创建的Runnable对象里面,包含了这个上下文对象,然后,继续使用这个上下文对象调用自己,有点类似于递归调用的意思。

 

 

同理,我们也可以用对象来封装我们的上下文调用信息,这里,我们要改造之前的抽象的AbstractTimeMethodCallerCountLimiter,将其改造为可以调度任务的对象,然后,我们需要定义这种调度的任务,使用接口来描述,这个接口里面只有一个方法,参数就是Context,这样,就能实现形如上面的调用方式了。

 

首先,定义这个任务接口,如下:

 

public interface LimitTask { public void run(LimitTaskContext limitTaskContext);}run方法里面的参数LimitTaskContext就是上下文对象,然后我们来定义调度这个LimitTask对象的调度类,如下:

 

 

public class LimitTaskScheduler { private long limitTime; private int acceptCallCount; private long watiExtendTime = 100; private int callCount; private Date startReRunTime = null; private LimitTaskContext limitTaskContext; public LimitTaskScheduler(final long limitTime, final int acceptCallCount) { super(); this.limitTime = limitTime; this.acceptCallCount = acceptCallCount; this.callCount = 0; this.limitTaskContext = new DefaultLimitTaskContext(this); } public void execute(LimitTask limitTask) { if (startReRunTime == null) { startReRunTime = new Date(); } int callCount = this.callCount; if (callCount < this.acceptCallCount) { limitTask.run(this.limitTaskContext); } else { Date now = new Date(); long nowTime = now.getTime(); Date firstRunTimeDate = this.startReRunTime; long firstRunTime = firstRunTimeDate.getTime(); long runTime = nowTime - firstRunTime; long waitTime = this.limitTime - runTime; if(waitTime > 0){ synchronized (this) { System.out.println("等待时间:" + waitTime); try { this.wait(waitTime + this.watiExtendTime); } catch (InterruptedException e) { e.printStackTrace(); } } } this.startReRunTime = new Date(); this.callCount = 0; System.out.println("调度清0"); limitTask.run(this.limitTaskContext); } } private class DefaultLimitTaskContext implements LimitTaskContext { private LimitTaskScheduler limitTaskScheduler; public DefaultLimitTaskContext(LimitTaskScheduler limitTaskScheduler) { super(); this.limitTaskScheduler = limitTaskScheduler; } @Override public void execute(LimitTask limitTask) { this.limitTaskScheduler.execute(limitTask); } }}
这里,主要的代码逻辑都与AbstractTimeMethodCallerCountLimiter差不多,就是内部定义了内部类DefaultLimitTaskContext,每当调用LimitTaskContext的run方法时,都将这个对象传到里面。接下来,定义LimitTaskContext,非常简单,就只有一个方法而已,

 

 

public interface LimitTaskContext { public void execute(LimitTask limitTask);}

 

 

三大组件LimitTaskContext,LimitTaskScheduler,LimitTask都定义好之后,我们来定义接口的本地Java模拟代码,来测试我们的程序,具体的抽象定义如下,

 

请求对象定义:

 

public interface Request { public Response request(RequestParam param); public Response requestByNextToken(String nextToken,RequestParam param);}
请求参数对象定义:

 

 

public interface RequestParam {}

 

 

请求响应对象定义:

 

public interface Response { boolean haveNext(); String getRequestNextToken();}
关于请求相关的实现类,就不贴出来了,都是非常简单的。接下来,我们定义我们的具体的LimitTask,

 

 

对于第一次请求而言,我们可以这么做:

 

public class FirstRequestLimitTask implements LimitTask{ @Override public void run(LimitTaskContext limitTaskContext) { System.out.println("run FirstRequestLimitTask"); RequestParam requestParam = new DefaultRequestParam(); Request request = new DefaultRequest(); Response response = request.request(requestParam); if(response.haveNext()){ String nextRequestToken = response.getRequestNextToken(); System.out.println("nextToken:"+nextRequestToken); limitTaskContext.execute(new NextRequestLimitTask(nextRequestToken,requestParam)); } }}
这里,当第一次请求,如果发现还有后续结果,那么就创建NextRequestLimitTask对象,将requestParam,和nextRequestToken传递进去,功能第二次请求使用,那么

 

NextRequestLimitTask对象的定义就是这样的:

 

 

public class NextRequestLimitTask implements LimitTask{ private String nextRequestToken; private RequestParam requestParam; public String getNextRequestToken() { return nextRequestToken; } public NextRequestLimitTask(String nextRequestToken, RequestParam requestParam) { super(); this.nextRequestToken = nextRequestToken; this.requestParam = requestParam; } @Override public void run(LimitTaskContext limitTaskContext) { System.out.println("run NextRequestLimitTask"); Request request = new DefaultRequest(); Response response = request.requestByNextToken(this.nextRequestToken,this.requestParam); if(response.haveNext()){ this.nextRequestToken = response.getRequestNextToken(); System.out.println("next token:"+this.nextRequestToken); limitTaskContext.execute(this); } }}

 

 

这个NextRequestLImitTask使用了第一次请求或获取的nextRequestToken,当调用run方法后,也许后面还有后续结果,那么就重新赋值nextRequestToken,然后,继续调用自己就可以了。

 

这样,结合FirstRequestLimitTask 和NextRequestLImitTask,就完成了我们调用接口的需求,我们来测试一下:

 

public static void main(String[] args) { LimitTaskScheduler scheduler = new LimitTaskScheduler(1*1000, 30); scheduler.execute(new FirstRequestLimitTask()); }

 

可以看到,确实是有等待的,代码运行Ok。

 

其实,第一次请求,和后续请求的代码可以合并在一起的,就是将nextRequestToken放在类字段上面,这样,就可以复用了,如下所示:

 

public class IterateSelfRequestLimitTask implements LimitTask { private String nextRequestToken; private RequestParam requestParam; public String getNextRequestToken() { return nextRequestToken; } public IterateSelfRequestLimitTask(RequestParam requestParam) { this.requestParam = requestParam; } @Override public void run(LimitTaskContext limitTaskContext) { System.out.println("run NextRequestLimitTask"); Request request = new DefaultRequest(); Response response = null; if(this.nextRequestToken == null){ response = request.request(this.requestParam); }else{ response = request.requestByNextToken(this.nextRequestToken, this.requestParam); } if (response.haveNext()) { this.nextRequestToken = response.getRequestNextToken(); System.out.println("next token:" + this.nextRequestToken); limitTaskContext.execute(this); } }}
类的作用和特性如它的名字IterateSelfRequestLimitTask,迭代自己的请求限制任务。调用代码也有了响应的改变:

 

 

public static void main(String[] args) { LimitTaskScheduler scheduler = new LimitTaskScheduler(1*1000, 30); scheduler.execute(new IterateSelfRequestLimitTask(new DefaultRequestParam())); }

 

虽然完成了功能,但是我不仅细想了一下对于API限速的真实实现场景,首先,为了支持高可用,API提供方,必定不可能会将API服务部署在一台机器上,必定会分布式,集

群,其次,API服务非常重要,作为基础设施,不可能会在Apache Tomcat层面去限速,有可能有一下组件去支持限速,这样,解耦API服务提供团队开发和基础组件开发团队,

带这这样的疑问,不免画了图,来说明这个问题。

 

在这种情况下,情况变得复杂了,需要分布式的请求队列,需要分布式的锁,而且,分布式的请求队列,和分布式的锁,也要支持高可用,编程的复杂度,提高了一个数量级

了。这时候,需要使用Zookeeper,Redis,或者消息中间件等组件了。

 

我没在互联网公司待过,不知道他们是如何做的,只能凭空猜测,虽然只是猜测,但也着实有意思。现在想来,对于一些事情,不能太认真,大家都是出来混口饭吃,面试的时

候,作为面试官,放人家一马,说不定人家以后就是第二个乔帮主;工作上,人家做的不是太好,睁一只眼,闭一只眼,何必太较真,跟自己过不去,老是惦记这自己的业绩,

却忽略了人之间的该有的东西,结果在这家公司能混好,老板赏识,领导信任,但是出去了,同事们却未必记得你;特别是感情上,更是别太认真,否则,受伤的总是自己。

 

转载于:https://www.cnblogs.com/weiguangyue/p/9349062.html

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

5个免费抠图工具,PS直接下岗

无论是设计师&#xff0c;还是摄影师&#xff0c;都需要经常给图片处理背景&#xff0c;用PS效率低&#xff0c;还繁琐。今天给大家推荐5个一键抠图神器&#xff0c;让你节省更多的时间和成本。有需要的朋友赶紧收藏&#xff01; 1、Removebg https://www.remove.bg/zh 一个免…

超简单的_ps抠图_在线抠图工具_智能抠图_速抠图

超简单的_ps抠图_在线抠图工具_智能抠图_速抠图 在线ps抠图_速抠图_智能抠图_sukoutu.com 关键词 在线抠图、ps抠图、智能抠图、一键抠图、钢笔抠图、图片处理、证件照换背景、一寸照制作、图片压缩 背景 一些用户在选择抠图时&#xff0c;通常会选择ps软件或者美图秀秀等pc端…

永久免费的抠图软件分享

有个小伙伴最近来短视频平台看到一些博主视频的封面图做的很搞笑很有吸引力&#xff0c;大致就是自拍的照片&#xff0c;做出有趣的动作然后抠图到反差感很大的背景图中&#xff0c;于是就自己也想尝试一下做出此类的图片来投稿&#xff0c;但是又不知道该怎么处理抠图和更换背…

如何抠图图片?这个方法值得点赞收藏

如何抠图图片&#xff1f;相信喜欢摄影的小伙伴们在日常生活中经常需要对图片进行抠图处理&#xff0c;对专业的小伙伴们来说抠图早已经是家常便饭。但是作为非专业人士&#xff0c;我们在日常生活中偶尔需要抠图的话&#xff0c;可能会不知道从何下手。其实抠图也并非你想象中…

如何抠图?抠图方法分享。

如何抠图&#xff1f;抠图是一种图像处理技术&#xff0c;通常用于将图像中的某个部分从背景中分离出来。在制作表情包时&#xff0c;通常需要先抠出人物或物品&#xff0c;然后将其放置在另一张图像或背景上。抠图可以通过各种软件来完成&#xff0c;很多小伙伴也不知道该采用…

Stable Diffusion 抠图工具使用小记

用stable diffusion有段时间了&#xff0c;最近使用了它的抠图工具 &#xff08;Remove background&#xff09;。这里是我对该工具的使用和记录。希望可以帮其他人了解相关内容。文末附100个Lora资源方便大家使用。 在图像处理中&#xff0c;抠图是一项非常重要的任务。传统的…

别花时间抠图了,赶紧试试这几个免抠图的PNG图片网站!

https://zhuanlan.zhihu.com/p/35284104 我们知道&#xff0c;做一份好的PPT&#xff0c;绝少不了一张好的图片素材&#xff0c;之前我也给大家推荐过很多&#xff0c;像pexel、pixabay、500px等。如果没看&#xff0c;可以看一下这篇推文《6个图片网站&#xff0c;带你找到全世…

怎样快速抠图ps图片?这些小妙招了解一下

最近有个做生活vlog记录的小伙伴发出提问&#xff0c;想给自己的照片抠图人像&#xff0c;更换个场景再p一些文字和效果做视频的封面图&#xff0c;其实一个视频除了内容&#xff0c;标题和封面图也至关重要&#xff0c;好的封面可以吸引观众想点击进去的心理&#xff0c;反之视…

怎么使用手机来抠图?这几种抠图方法手机就能操作

怎么使用手机来进行抠图呢&#xff1f;使用手机抠图应用程序&#xff0c;我们无需将图片转移到电脑上进行编辑&#xff0c;只需在手机上点击几下&#xff0c;即可完成图片抠图。这种便捷的操作方式&#xff0c;能够大大提高我们的工作效率。可以随时随地进行抠图操作&#xff0…

如何简单抠图?在手机上就可以完成抠图

如何简单抠图&#xff1f;我们在作图的时候大都需要进行抠图操作&#xff0c;将一些好看的元素抠出来&#xff0c;放到海报或者一些图片当中&#xff0c;可以说抠图已经成为我们修图中的十分重要的部分。但是可能很多小伙伴们还不是很清楚如何更快抠图&#xff0c;这里小编就来…

PS常用的三种抠图方法,能应对99%的抠图场景

使用工具&#xff1a;PS CC2018 方法&#xff1a;通道抠图、蒙版抠图、选择并遮住 方法一&#xff1a;通道抠图&#xff08;主体颜色和背景色不能太接近&#xff09; 步骤1 &#xff1a;使用套索工具将主体部分选中&#xff0c;建立选取→ctrlj&#xff0c;将建立选取的部分复…

GIMP抠图

The GNU Image Manipulation Program&#xff08;简称GIMP&#xff09;&#xff0c;是免费的&#xff0c;分布式的图片润饰、图像制作和处理软件&#xff0c;用GTK编写的图像编辑处理程序&#xff0c;被称为Linux下的Photoshop&#xff0c;但是也有Windows版本的。 按颜色选择…

最好用的PS一键智能抠图插件,3秒就能扣好一张图

在我们生活和工作中&#xff0c;很多时候都需要抠图&#xff0c;咱们之前也分享过不少PS抠图插件&#xff0c;但是那些抠图插件不是安装太麻烦&#xff0c;就是使用有点小复杂&#xff0c;所以今天给大家分享一款PS一键智能抠图插件 相比之前推荐的Topaz Mask AI抠图插件&#…

PS人物一键抠图,实现照片背景更换

标题PS人物一键抠图&#xff0c;实现照片背景更换 1.打开PS,把需要抠图的图片放进去 2.在击 选择–>主体 &#xff0c; 3.可以看到人物已被选中&#xff0c;点击 选择被遮住 4.之后在这个里面修改下头发边缘位置 5.调整完后&#xff0c;输出到带有图层蒙版的图层&#…

人像抠图怎么抠出来?这几种抠图方法一看就会

在广告设计中&#xff0c;需要将产品或服务的图片与人像进行合成。为了让图片看起来更加自然&#xff0c;需要将人像抠出来并进行后期处理。比如&#xff0c;在一张广告海报中&#xff0c;需要将产品的图片与人像合成&#xff0c;使整张海报看起来更加真实&#xff0c;让观众更…

怎么把人物抠图到另一个背景上?这几种抠图方法很实用

怎么把人物抠图放到另一个背景上呢&#xff1f;抠出人物图像可以轻松地将人物放入不同的背景中&#xff0c;从而创造出更加丰富的视觉效果。通过更改背景&#xff0c;可以改变照片的整体氛围&#xff0c;带来不同的感受和体验。也能更好地突出人物本身&#xff0c;使其更加突出…

物品怎么抠图?手把手教你快速抠图

物品怎么抠图&#xff1f;很多时候我们经常需要对人像进行抠图换背景等操作&#xff0c;但是有的时候我们也需要对物品进行抠图。商家在发布物品宣传图的时候&#xff0c;如果对拍摄的物品的背景不满意。这个时候重新拍摄十分耽误时间&#xff0c;其实完成可以直接将物品抠出来…

使用GIMP抠图

文章目录 使用GIMP进行抠图&#xff0c;然后再使用一些工具例如python库PIL进行paste&#xff0c;这样就可以实现数据增广。 下面详细说一下步骤&#xff1a; 1、在GIMP软件中打开图片&#xff1b; 如图&#xff0c; 2、比如把上图中的银行卡给抠下来&#xff0c;可以长按 ctr…

抠图怎么抠?这几种抠图方法分享给大家

抠图怎么抠呢&#xff1f;将照片中的人物抠出来是一种很有用的技巧&#xff0c;可以在很多场景下应用。例如&#xff0c;如果您想制作一个海报或广告&#xff0c;将人物从原始照片中抠出来可以让他们更加突出&#xff0c;吸引更多的眼球。如果您需要为自己或您的公司制作名片&a…

如何人物抠图?介绍三种人物抠图的方法

如何将照片里的人物从照片里抠出来呢&#xff1f;在广告制作、 社交媒体、商品展示、影视制作等场景下&#xff0c;需要将产品图片与人物图片进行组合&#xff0c;这时就需要将人物从原始图片中抠出来&#xff0c;然后放置到广告背景中。今天来介绍三种方法&#xff0c;来满足你…