一文教你如何实现并发请求的失败自动重试及重试次数限制

需求

在并发接口请求的时候,能够自动对失败的请求进行重发尝试(超过指定重试次数则不再重试),并将最终的结果返回(包含每个请求是否成功、返回结果)

核心思路

代码实现

使用案例

为了演示我们代码的最终实现效果,我们使用如下的案例:

js 复制代码
function request1() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('请求完毕111');
      resolve(111)
    }, 1000);
  })
}
function request2() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('请求完毕222');
      reject(222)
    }, 2000);
  })
}
function request3() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('请求完毕333');
      reject(333)
    }, 3000)
  })
}

如上存在3个异步请求,我们想要并发发送,并获取到它们的请求结果,理想的方式是这样的:

js 复制代码
new RequestParallelWithRetry([ request1, request2, request3 ]).send().then((value) => {
  console.log(value);
})

因此,这里我们需要实现一个RequestParallelWithRetry

传入方法标记

根据我们上面提出的思路,我们需要将传入的请求方法进行标记并保存它们的关系,便于后续请求失败时可以利用这个标记来再次调用这个方法

js 复制代码
class RequestParallelWithRetry {
  requestList
  retryCount
  map = {}
  result = []
  constructor(requestList = [], retryCount = 3) {
    this.requestList = requestList
    this.retryCount = retryCount
    // 需要给每个方法添加标记
    this.requestList.forEach((item, index) => {
      item._index = index
      this.map[ index ] = item
    })
    console.log(this.map);
  }
}

这里我们使用传入的请求列表下标作为标记,并保存起来,打印出来的map结果如下:

并发请求发送

之后我们需要实现RequestParallelWithRetry类的send方法,它用来并发发送请求,并且要求能够得知每个请求的执行结果。因为我们这里请求的返回结果都是一个promise对象,因此我们可以利用Promise.allSettled方法来获取所有promise的最终结果:

js 复制代码
send() {
  const queue = []
  this.requestList.forEach(request => {
    queue.push(request())
  })
  Promise.allSettled(queue).then((value) => {
    console.log('所有请求都完成了', value)
  })
}

此时,调用new RequestParallelWithRetry([ request1, request2, request3 ]).send()将得到如下的打印结果:

可以看到:

  • request2request3都请求失败了(返回rejected
  • 我们此时需要做的其实是将这些所有状态为rejected的请求再次执行直到所有的请求都成功(fulfilled)了(先不考虑重试次数限制的情况)
  • 因此,我们需要递归执行这个过程(发送请求->找到失败的请求->再次发送)

包装请求的执行结果

我们已经知道了我们重复执行那些失败的请求直到成功,但是还存在一个问题:现在我们只能知道失败请求的结果,但是我们无法根据这个结果来找到原先执行请求的方法。因此,我们需要对传入的请求进行一层包装,使得执行的返回结果中包含一个标记,我们能根据这个标记找到这个请求方法:

js 复制代码
send() {
  const queue = []
  this.requestList.forEach(request => {
    queue.push(this._wrapRequest(request))
  })
  Promise.allSettled(queue).then((value) => {
    console.log('所有请求都完成了', value)
  })
}
_wrapRequest(request) {
  return new Promise((resolve, reject) => {
    request().then((value) => {
      resolve({
        value,
        index: request._index
      })
    }).catch(reason => {
      reject({
        reason,
        index: request._index
      })
    })
  })
}

此时,我们就能在请求结果中获取到原先请求方法的标记index了,利用这个标记和已经保存过的map关系映射,就能获取到原先的请求方法,从而再次调用

请求失败再次调用

js 复制代码
send() {
  let queue = []
  this.requestList.forEach(request => {
    queue.push(this._wrapRequest(request))
  })
  const sendAllSettled = () => {
    Promise.allSettled(queue).then((value) => {
      queue = []
      console.log('所有请求都完成了', value);
      // 检查是否存在失败的请求
      value.forEach(result => {
        if (result.status === "rejected") {
          const { reason, index } = result.reason
          const request = this.map[ index ]
          queue.push(this._wrapRequest(request))
        }
      })
      //  检查队列中是否还需要发送请求
      if (queue.length) {
        sendAllSettled()
      }
    })
  }

  sendAllSettled()
}

这里,我们在send方法内部递归调用sendAllSettled方法,sendAllSettled方法做的事情很简单:

  1. queue队列中的请求发送出去
  2. 将失败的请求重新添加到队列中,再次发送

重试次数限制

在上面的代码中,我们已经实现了并发请求失败时自动重新尝试,但是在实际开发中,我们并不能让它无限制地重试下去,而是应该指定重试的次数,一旦超过这个次数了,就不再重试

具体的实现也很简单,只需要给每个请求方法添加一个记录重试次数的属性_retryCount即可,一旦这个_retryCount超过retryCount就不再重试了

具体代码如下:

js 复制代码
send() {
  let queue = []
  this.requestList.forEach(request => {
    request._retryCount = 0
    queue.push(this._wrapRequest(request))
  })
  const sendAllSettled = () => {
    Promise.allSettled(queue).then((value) => {
      queue = []
      console.log('所有请求都完成了', value);
      // 检查是否存在失败的请求
      value.forEach(result => {
        if (result.status === "rejected") {
          const { reason, index } = result.reason
          const request = this.map[ index ]
          if (request._retryCount < this.retryCount) {
            queue.push(this._wrapRequest(request))
            request._retryCount++
          }
        }
      })
      //  检查队列中是否还需要发送请求
      if (queue.length) {
        sendAllSettled()
      }
    })
  }

  sendAllSettled()
}

并发请求执行结果返回

上面的代码中,我们已经实现了并发请求的失败重试以及重试次数的限制了,最后我们需要将并发请求的最终执行结果返回给用户

由于并发请求是一系列的异步操作,因此我们对于send方法的返回结果也应该是一个promise对象,这个对象包含了每个请求的执行结果(成功或失败)和返回结果

具体实现如下:

js 复制代码
send() {
  return new Promise((resolve) => {
    // 1. 将所有的网络请求进行包装,使得其返回结果中携带有_index
    // 2. 发送所有的网络请求
    let queue = []
    this.requestList.forEach(request => {
      // 3. 记录每个请求的重试次数
      request._retryCount = 0
      queue.push(this._wrapRequest(request))
    })

    const sendAllSettled = () => {
      Promise.allSettled(queue).then((value) => {
        // 3. 清除发送队列
        queue = []
        console.log('所有请求都完成了', value);
        // 4. 检查是否存在失败的请求
        value.forEach(result => {
          if (result.status === "fulfilled") {
            const { value, index } = result.value
            this.result[ index ] = { value, status: 'fulfilled' }
          }
          else if (result.status === "rejected") {
            const { reason, index } = result.reason
            const request = this.map[ index ]
            if (request._retryCount < this.retryCount) {
              queue.push(this._wrapRequest(request))
              request._retryCount++
            } else {
              this.result[ index ] = { reason, status: 'rejected' }
            }
          }
        })
        //  5. 检查队列中是否还需要发送请求
        if (queue.length) {
          sendAllSettled()
        } else {
          resolve(this.result)
        }
      })
    }

    sendAllSettled()

  })
}

最终用户的调用形式和执行结果会是这样的:

js 复制代码
new RequestParallelWithRetry([ request1, request2, request3 ]).send().then((value) => {
  console.log(value);
})
相关推荐
William数据分析3 分钟前
JavaScript 语法零基础入门:从变量到异步(附 Python 语法对比)
开发语言·javascript·python
疯狂的沙粒9 分钟前
Vue 前端大屏做多端屏幕适配时,如何让其自动适配多种不同尺寸的屏幕?
前端·javascript·vue.js
范小多13 分钟前
24小时学会Python Visual code +Python Playwright通过谷歌浏览器取控件元素(连载、十一)
服务器·前端·python
ooolmf14 分钟前
matlab2024读取温度01
java·前端·javascript
打工人小夏15 分钟前
前端vue3项目使用nprogress动画组件,实现页面加载动画
前端
一颗宁檬不酸17 分钟前
前端农业商城中产品产地溯源功能的实现
前端
李少兄24 分钟前
深入理解前端中的透视(Perspective)
前端·css
席之郎小果冻26 分钟前
【03】【创建型】【聊一聊,单例模式】
开发语言·javascript·单例模式
江公望34 分钟前
HTML5 History 模式 5分钟讲清楚
前端·html·html5
云和数据.ChenGuang40 分钟前
Zabbix Web 界面安装时**无法自动创建配置文件 `zabbix.conf.php`** 的问题
前端·zabbix·运维技术·数据库运维工程师·运维教程