设计一个支持并发的前端接口缓存

这篇具有很好参考价值的文章主要介绍了设计一个支持并发的前端接口缓存。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录​​​​​​​

缓存池

并发缓存

问题

思考

优化🤔

总结

最后


缓存池

        缓存池不过就是一个map,存储接口数据的地方,将接口的路径和参数拼到一块作为key,数据作为value存起来罢了,这个咱谁都会。

const cacheMap = new Map();

封装一下调用接口的方法,调用时先走咱们缓存数据。

import axios, { AxiosRequestConfig } from 'axios'

// 先来一个简简单单的发送
export function sendRequest(request: AxiosRequestConfig) {
  return axios(request)
}

然后加上咱们的缓存

import axios, { AxiosRequestConfig } from 'axios'
import qs from 'qs'

const cacheMap = new Map()

interface MyRequestConfig extends AxiosRequestConfig {
  needCache?: boolean
}

// 这里用params是因为params是 GET 方式传的参数,我们的缓存一般都是 GET 接口用的
function generateCacheKey(config: MyRequestConfig) {
  return config.url + '?' + qs.stringify(config.params)
}

export function sendRequest(request: MyRequestConfig) {
  const cacheKey = generateCacheKey(request)
  // 判断是否需要缓存,并且缓存池中有值时,返回缓存池中的值
  if (request.needCache && cacheMap.has(cacheKey)) {
    return Promise.resolve(cacheMap.get(cacheKey))
  }
  return axios(request).then((res) => {
    // 这里简单判断一下,200就算成功了,不管里面的data的code啥的了
    if (res.status === 200) {
      cacheMap.set(cacheKey, res.data)
    }
    return res
  })
}

然后调用

const getArticleList = (params: any) =>
  sendRequest({
    needCache: true,
    url: '/article/list',
    method: 'get',
    params
  })

getArticleList({
  page: 1,
  pageSize: 10
}).then((res) => {
  console.log(res)
})

        这个部分就很简单,我们在调接口时给一个needCache的标记,然后调完接口如果成功的话,就会将数据放到cacheMap中去,下次再调用的话,就直接返回缓存中的数据。

并发缓存

        上面的虽然看似实现了缓存,不管我们调用几次,都只会发送一次请求,剩下的都会走缓存。但是真的是这样吗?

getArticleList({
  page: 1,
  pageSize: 10
}).then((res) => {
  console.log(res)
})
getArticleList({
  page: 1,
  pageSize: 10
}).then((res) => {
  console.log(res)
})

        其实这样,就可以测出,我们的虽然设计了缓存,但是请求还是发送了两次,这是因为我们第二次请求发出时,第一次请求还没完成,也就没给缓存池里放数据,所以第二次请求没命中缓存,也就又发了一次。

问题

        那么,有没有一种办法让第二次请求等待第一次请求调用完成,然后再一块返回呢?

思考

        有了!我们写个定时器就好了呀,比如我们可以给第二次请求加个定时器,定时器时间到了再去cacheMap中查一遍有没有缓存数据,没有的话可能是第一个请求还没好,再等几秒试试!

        可是这样的话,第一个请求的时候也会在原地等呀!😒

        那这样的话,让第一个请求在一个地方贴个告示不就好了,就像上厕所的时候在门口挂个牌子一样!😎

// 存储缓存当前状态,相当于挂牌子的地方
const statusMap = new Map<string, 'pending' | 'complete'>();

export function sendRequest(request: MyRequestConfig) {
  const cacheKey = generateCacheKey(request)

  // 判断是否需要缓存
  if (request.needCache) {
    if (statusMap.has(cacheKey)) {
      const currentStatus = statusMap.get(cacheKey)

      // 判断当前的接口缓存状态,如果是 complete ,则代表缓存完成
      if (currentStatus === 'complete') {
        return Promise.resolve(cacheMap.get(cacheKey))
      }

      // 如果是 pending ,则代表正在请求中,这里就等个三秒,然后再来一次看看情况
      if (currentStatus === 'pending') {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            sendRequest(request).then(resolve, reject)
          }, 3000)
        })
      }
    }

    statusMap.set(cacheKey, 'pending')
  }

  return axios(request).then((res) => {
    // 这里简单判断一下,200就算成功了,不管里面的data的code啥的了
    if (res.status === 200) {
      statusMap.set(cacheKey, 'complete')
      cacheMap.set(cacheKey, res)
    }
    return res
  })
}

试试效果

getArticleList({
    page: 1,
    pageSize: 10
}).then((res) => {
    console.log(res)
})
getArticleList({
    page: 1,
    pageSize: 10
}).then((res) => {
    console.log(res)
})

设计一个支持并发的前端接口缓存

设计一个支持并发的前端接口缓存

成了!这里真的做到了,可以看到我们这里打印了两次,但是只发了一次请求。

优化🤔

        可是用setTimeout等待还是不太优雅,如果第一个请求能在3s以内完成还行,用户等待的时间还不算太久,还能忍受。可如果是3.1s的话,第二个接口用户可就白白等了6s之久,那么,有没有一种办法,能让第一个接口完成后,接着就通知第二个接口返回数据呢?

        等待,通知,这种场景我们写代码用的最多的就是回调了,但是这次用的是promise啊,而且还是毫不相干的两个promise。等等!callbackpromisepromise本身就是callback实现的!promisethen会在resole被调用时调用,这样的话,我们可以将第二个请求的resole放在一个callback里,然后在第一个请求完成的时候,调用这个callback!🥳

// 定义一下回调的格式
interface RequestCallback {
  onSuccess: (data: any) => void
  onError: (error: any) => void
}

// 存放等待状态的请求回调
const callbackMap = new Map<string, RequestCallback[]>()

export function sendRequest(request: MyRequestConfig) {
  const cacheKey = generateCacheKey(request)

  // 判断是否需要缓存
  if (request.needCache) {
    if (statusMap.has(cacheKey)) {
      const currentStatus = statusMap.get(cacheKey)

      // 判断当前的接口缓存状态,如果是 complete ,则代表缓存完成
      if (currentStatus === 'complete') {
        return Promise.resolve(cacheMap.get(cacheKey))
      }

      // 如果是 pending ,则代表正在请求中,这里放入回调函数
      if (currentStatus === 'pending') {
        return new Promise((resolve, reject) => {
          if (callbackMap.has(cacheKey)) {
            callbackMap.get(cacheKey)!.push({
              onSuccess: resolve,
              onError: reject
            })
          } else {
            callbackMap.set(cacheKey, [
              {
                onSuccess: resolve,
                onError: reject
              }
            ])
          }
        })
      }
    }

    statusMap.set(cacheKey, 'pending')
  }

  return axios(request).then(
    (res) => {
      // 这里简单判断一下,200就算成功了,不管里面的data的code啥的了
      if (res.status === 200) {
        statusMap.set(cacheKey, 'complete')
        cacheMap.set(cacheKey, res)
      } else {
        // 不成功的情况下删掉 statusMap 中的状态,能让下次请求重新请求
        statusMap.delete(cacheKey)
      }
      // 这里触发resolve的回调函数
      if (callbackMap.has(cacheKey)) {
        callbackMap.get(cacheKey)!.forEach((callback) => {
          callback.onSuccess(res)
        })
        // 调用完成之后清掉,用不到了
        callbackMap.delete(cacheKey)
      }
      return res
    },
    (error) => {
      // 不成功的情况下删掉 statusMap 中的状态,能让下次请求重新请求
      statusMap.delete(cacheKey)
      // 这里触发reject的回调函数
      if (callbackMap.has(cacheKey)) {
        callbackMap.get(cacheKey)!.forEach((callback) => {
          callback.onError(error)
        })
        // 调用完成之后也清掉
        callbackMap.delete(cacheKey)
      }
      // 这里要返回 Promise.reject(error),才能被catch捕捉到
      return Promise.reject(error)
    }
  )
}

        在判断到当前请求状态是pending时,将promiseresolereject放入回调队列中,等待被触发调用。然后在请求完成时,触发对应的请求队列。

试一下

getArticleList({
    page: 1,
    pageSize: 10
}).then((res) => {
    console.log(res)
})
getArticleList({
    page: 1,
    pageSize: 10
}).then((res) => {
    console.log(res)
})

设计一个支持并发的前端接口缓存

设计一个支持并发的前端接口缓存

OK,完成了。

再试一下失败的时候

getArticleList({
    page: 1,
    pageSize: 10
}).then(
    (res) => {
      console.log(res)
    },
    (error) => {
      console.error(error)
    }
)
getArticleList({
    page: 1,
    pageSize: 10
}).then(
    (res) => {
      console.log(res)
    },
    (error) => {
      console.error(error)
    }
)

设计一个支持并发的前端接口缓存

        OK,两个都失败了。(但是这里的error2早于error1打印,你知道是啥原因吗?🤔)

总结

    promise封装并发缓存到这里就结束啦,不过看到这里你可能会觉着没啥用处,但是其实这也是我碰到的一个需求才延申出来的,当时的场景是一个页面里有好几个下拉选择框,选项都是接口提供的常量。但是只接口提供了一个接口返回这些常量,前端拿到以后自己再根据类型挑出来,所以这种情况我们肯定不能每个下拉框都去调一次接口,只能是寄托缓存机制了。

        这种写法,在另一种场景下也很好用,比如将需要用户操作的流程封装成promise。例如,A页面点击A按钮,出现一个B弹窗,弹窗里有B按钮,用户点击B按钮之后关闭弹窗,再弹出C弹窗C按钮,点击C之后流程完成,这种情况就很适合将每个弹窗里的操作流程都封装成一个promise,最外面的A页面只需要连着调用这几个promise就可以了,而不需要维护控制这几个弹窗显示隐藏的变量了。

放一下全部代码

import axios, { AxiosRequestConfig } from 'axios'
import qs from 'qs'

// 存储缓存数据
const cacheMap = new Map()

// 存储缓存当前状态
const statusMap = new Map<string, 'pending' | 'complete'>()

// 定义一下回调的格式
interface RequestCallback {
  onSuccess: (data: any) => void
  onError: (error: any) => void
}

// 存放等待状态的请求回调
const callbackMap = new Map<string, RequestCallback[]>()

interface MyRequestConfig extends AxiosRequestConfig {
  needCache?: boolean
}

// 这里用params是因为params是 GET 方式穿的参数,我们的缓存一般都是 GET 接口用的
function generateCacheKey(config: MyRequestConfig) {
  return config.url + '?' + qs.stringify(config.params)
}

export function sendRequest(request: MyRequestConfig) {
  const cacheKey = generateCacheKey(request)

  // 判断是否需要缓存
  if (request.needCache) {
    if (statusMap.has(cacheKey)) {
      const currentStatus = statusMap.get(cacheKey)

      // 判断当前的接口缓存状态,如果是 complete ,则代表缓存完成
      if (currentStatus === 'complete') {
        return Promise.resolve(cacheMap.get(cacheKey))
      }

      // 如果是 pending ,则代表正在请求中,这里放入回调函数
      if (currentStatus === 'pending') {
        return new Promise((resolve, reject) => {
          if (callbackMap.has(cacheKey)) {
            callbackMap.get(cacheKey)!.push({
              onSuccess: resolve,
              onError: reject
            })
          } else {
            callbackMap.set(cacheKey, [
              {
                onSuccess: resolve,
                onError: reject
              }
            ])
          }
        })
      }
    }

    statusMap.set(cacheKey, 'pending')
  }

  return axios(request).then(
    (res) => {
      // 这里简单判断一下,200就算成功了,不管里面的data的code啥的了
      if (res.status === 200) {
        statusMap.set(cacheKey, 'complete')
        cacheMap.set(cacheKey, res)
      } else {
        // 不成功的情况下删掉 statusMap 中的状态,能让下次请求重新请求
        statusMap.delete(cacheKey)
      }
      // 这里触发resolve的回调函数
      if (callbackMap.has(cacheKey)) {
        callbackMap.get(cacheKey)!.forEach((callback) => {
          callback.onSuccess(res)
        })
        // 调用完成之后清掉,用不到了
        callbackMap.delete(cacheKey)
      }
      return res
    },
    (error) => {
      // 不成功的情况下删掉 statusMap 中的状态,能让下次请求重新请求
      statusMap.delete(cacheKey)
      // 这里触发reject的回调函数
      if (callbackMap.has(cacheKey)) {
        callbackMap.get(cacheKey)!.forEach((callback) => {
          callback.onError(error)
        })
        // 调用完成之后也清掉
        callbackMap.delete(cacheKey)
      }
      return Promise.reject(error)
    }
  )
}

const getArticleList = (params: any) =>
  sendRequest({
    needCache: true,
    baseURL: 'http://localhost:8088',
    url: '/article/blogList',
    method: 'get',
    params
  })

export function testApi() {
  getArticleList({
    page: 1,
    pageSize: 10
  }).then(
    (res) => {
      console.log(res)
    },
    (error) => {
      console.error('error1:', error)
    }
  )
  getArticleList({
    page: 1,
    pageSize: 10
  }).then(
    (res) => {
      console.log(res)
    },
    (error) => {
      console.error('error2:', error)
    }
  )
}

最后

        对请求结果是否成功那里处理的比较简陋,项目里用到的话根据自己情况来。文章来源地址https://www.toymoban.com/news/detail-471282.html

到了这里,关于设计一个支持并发的前端接口缓存的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 架构设计第十讲:架构之高并发:缓存

    高并发实现的三板斧:缓存,限流和降级 。缓存在高并发系统中有者极其广阔的应用,需要重点掌握,本文是架构设计第10讲,重点介绍下缓存及其实现

    2024年02月12日
    浏览(43)
  • 简易版前端项目离线方案-接口及页面离线缓存

    为了避免后端流控、崩溃等异常而无法访问的情况,就需要将接口和页面的静态资源缓存在用户的浏览器本地,这样一来,就算后端服务不可达,前端依旧能有正常的页面显示操作反馈,大部分用户无法感知到系统出现了故障. 这个虽然听起来高大上,其实就是前端服务和后端服务分

    2024年02月03日
    浏览(44)
  • 【Redis】电商项目秒杀问题之下单接口优化:Redis缓存、MQ以及lua脚本优化高并发背景下的秒杀下单问题

    目录 一、优化思路 二、缓存库存与订单 1、库存缓存的redis数据结构 2、订单信息缓存的redis数据结构 三、整体流程 四、lua脚本确保权限校验操作的原子性 【Redis】电商项目秒杀问题之超卖问题与一人一单问题_1373i的博客-CSDN博客 https://blog.csdn.net/qq_61903414/article/details/1305689

    2024年02月05日
    浏览(48)
  • 如何设计一个高并发系统?

    所谓高并发系统,是指能同时处理大量并发请求,并及时响应,从而保证系统的高性能和高可用 那么我们在设计一个高并发系统时,应该考虑哪些方面呢? 1. 搭建集群 如果你只部署一个应用,只部署一台服务器,那抗住的流量请求是非常有限的。并且,单体的应用,有单点

    2024年02月01日
    浏览(40)
  • 如何设计一个高并发系统

    目录 如何理解高并发系统 1. 分而治之,横向扩展 2. 微服务拆分(系统拆分) 3. 分库分表 4. 池化技术 5. 主从分离 6. 使用缓存 7. CDN——加速静态资源访问 8. 消息队列——削锋 9. ElasticSearch 10. 降级熔断 11. 限流 12. 异步 13. 常规的优化 14. 压力测试确定系统瓶颈 15. 应对突发流

    2023年04月21日
    浏览(34)
  • 从 Context 看 Go 设计模式:接口、封装和并发控制

    在 Go 语言中, context 包是并发编程的核心,用于传递取消信号和请求范围的值。但其传值机制,特别是为什么不通过指针传递,而是通过接口,引发了我的一些思考。 考虑以下典型的代码片段: 这段代码展示了在 Go 中创建和传递 context 的简单用法。但背后的设计理念和实现

    2024年01月20日
    浏览(42)
  • 「并发编程实战」接口幂等性设计的最佳实现(8种实现方案)

    文章参考: 实战!聊聊幂等设计 基于幂等表思想的幂等实践 追忆四年前:一段关于我被外企CTO用登录注册吊打的不堪往事 弹力设计篇之“幂等性设计” 幂等是一个数学与计算机科学概念。 在数学中,幂等用函数表达式就是: f(x) = f(f(x)) 。比如求绝对值的函数,就是幂等的

    2024年01月22日
    浏览(44)
  • 设计一个LRU(最近最少使用)缓存

    约束和假设 我们正在缓存什么? 我们正在缓存Web Query的结果 我们可以假设输入是有效的,还是需要对其验证? 假设输入是有效的 我们可以假设它适应内存吗? 对 编码实现

    2024年01月24日
    浏览(66)
  • 设计一个高流量高并发的系统需要关注哪些点

    我相信每一位开发同学多多少少都想参与或负责一个高用户、高访问、高并发的系统吧😁。一来可以增加自己实际的项目经验,有应对高并发场景的解决方案,二来是有个高并发的项目经验无疑是自己简历的一个大大的加分项。但是奈何很多人都没有机会可以参与这样的项目

    2023年04月16日
    浏览(37)
  • 如何设计一个合格的高并发秒杀系统

    在前面的文章中,详细阐述了建设秒杀系统的目标与存在的挑战,并且简单罗列了如何应对这些挑战的方式。本章,就详细阐述对秒杀系统存在挑战的应对之道,最终构建出兼具高并发、高性能和高可用的秒杀系统。心中不仅了解建设秒杀系统存在的挑战,更清楚的知道这些

    2024年02月05日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包