前言
最近在项目中接触了一个需求,需求中有一部分是需要调用一个 “每秒有调用次数限制的接口” ,第一次碰到,感觉比较有意思,记录一下需求以及和小伙伴们一起构思的解决方案分享给大家。
1、需求描述
公司最近在做一个项目,需要调用第三方的接口获取订单数据。但是第三方系统提供出来的接口有调用限制,每秒只能被请求6次,如果超过这个限制,就会报异常。需求要求在保证效率的前提下(每秒调用6次需要打满),还需要这个功能高可用(少报错甚至不报错)。
2、问题所在
问题1:多线程并发时无法控制每秒调用总量问题:
如果说这个需求,在我们学生时代开发的demo中出现,我觉得这都不会算是问题。因为,毕竟你只有一台机跑项目,而且自己写的项目基本上也是单线程。简单的将任务进行分组调用,然后记录时间即可,伪代码如下:
/**
* 调用第三方接口获取订单数据方法
* @param userReqs:用户请求信息列表
*/
public Result getOrdersByThird(List<UserRequest> userReqs) throws InterruptedException {
List<UserRequest> userReqGroup = new ArrayList<>();
for (UserRequest userReq : userReqs) {
userReqGroup.add(userReq);
// 将用户请求分为每6个一组,一旦达到了6个就开始调用第三方接口
if(userReqGroup.size() == 6){
long startTime = System.currentTimeMillis();
for (UserRequest callRequest : userReqGroup) {
开始调用第三方接口,详细代码省略.....
}
long diff = System.currentTimeMillis() - startTime;
// 如果处理一组请求时间小于1s(1000 ms),让线程休眠快出的时间;否则代码接着往下执行即可。
if(diff < 1000){
Thread.sleep(diff);
}
// 每一组请求调用完成之后,清空分组,让分组列表重新计数
userReqGroup.clear();
}
}
// 返回处理结果,代码省略....
}
可是,企业级开发,代码肯定是运行在多台机器上的,且每一台机器都会多线程去跑,多机器多线程的话,上述代码肯定就会有问题,因为你只能保证当前在这台机器上执行这段代码的线程调用不超过6次/s,但是无法保证每秒调用的总量不超过这个阈值。
3、解决方案
前提条件: 调用第三方的代码,我们公司是用两台机去跑的,且每台机的定时任务中配置了四个线程。
方案一: 既然问题是来源于多机器多线程并发的时候,无法控制每秒调用的总量,那我们是不是可以考虑分布式锁呢,部署了代码的每台机器的每个线程,调用之前先尝试去获取分布式锁,如果拿到了就执行调用代码,没拿到就休眠一会再尝试。这里采用 redis 的 setnx() 做分布式锁书写代码案例。
/**
* 调用第三方接口获取订单数据方法
* @param userReqs:用户请求信息列表
*/
public Result handOrders(List<UserRequest> userReqs) throws InterruptedException {
// 首先按照每组6个,将请求信息进行分组
List<UserRequest> userReqGroup = new ArrayList<>(6);
for (UserRequest userReq : userReqs) {
userReqGroup.add(userReq);
// 将用户请求分为每6个一组,一旦达到了6个就开始调用第三方接口
if(userReqGroup.size() == 6){
getOrdersByThird(userReqGroup);
// 每一组请求调用完成之后,清空分组,让分组列表重新计数
userReqGroup.clear();
}
}
// 返回处理结果,代码省略....
}
public void getOrdersByThird(List<UserRequest> userReqGroup) throws InterruptedException {
// 尝试获取redis分布式锁
if(redis.setnx(key_mutex, "1")){
// 根据业务耗时设置互斥锁3min过期时间,防止因为机器宕机导致死锁
redis.expire(key_mutex, 3 * 60);
long startTime = System.currentTimeMillis();
for (UserRequest callRequest : userReqGroup) {
开始调用第三方接口,详细代码省略.....
}
long diff = System.currentTimeMillis() - startTime;
// 如果处理一组请求时间小于1s(1000 ms),让线程休眠快出的时间;否则代码接着往下执行即可。
if(diff < 1000){
Thread.sleep(diff);
}
// 使用完成之后把互斥锁(键给删掉)释放掉
redis.delKey(key_mutex);
} else {
// 如果获取不到,就休息50毫秒重试
Thread.sleep(50L);
getOrdersByThird(userReqGroup);
}
}
方案二: 每次调用完第三方接口之后,让线程休眠1s,但是这样仍然会可能出现问题。比如说:我机器A让线程1去跑这段代码,然后20ms跑完了,睡眠1s,然后机器A可能去调度线程2跑这段代码,同样也是花20ms跑完,睡眠1s…以此类推,因为公司跑这段代码的最大线程数是8,所以仍然可能会出现在1s内超过6次【阈值】调用的情况的。
那如果出现这种机制咋整呢?简单!对于超过6次限制的调用请求,虽然说返回失败了,但是我们可以在代码里面捕获,然后提供重试机制不就好了嘛,这时可能会有小伙伴说了,如果重试中的请求也失败了咋整呢?那就再多重试一次呗,我们一般会提供2-3次重试机制,对付公司同时并发量不高的情况下,应该还是够用了的。因为代码比较简单,这里就只是阐述解决方案了。
分享到此就结束了,完结撒花~~~~
我是杰哥,一个在IT行业中正在不断学习的程序员。 欢迎各位说话好听的人才们一键三连,你们的支持是我更新的最大动力,咱们下期见~
文章持续更新,可以微信搜索「 杰哥是真想教会你 」第一时间阅读,回复【面试】有我准备的一些在精不在多的面试资料。