Api限速

来源:互联网 发布:杜兰特数据统计库 编辑:程序博客网 时间:2024/04/28 11:12

最近遇到几个很有意思的接口,这些接口具有调用速率的限制,比如说一个接口具有每小时请求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次调用开始时,获得当前时间t2t2 - 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;}@Overrideprotected 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{@Overridepublic 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;}@Overridepublic 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;}@Overridepublic 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{@Overridepublic 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;}@Overridepublic 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;}@Overridepublic 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,或者消息中间件等组件了。


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

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

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











原创粉丝点击