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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值