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

目录​​​​​​​

缓存池

并发缓存

问题

思考

优化🤔

总结

最后


缓存池

        缓存池不过就是一个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)
    }
  )
}

最后

        对请求结果是否成功那里处理的比较简陋,项目里用到的话根据自己情况来。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
#import <Foundation/Foundation.h> typedef void(^SuccessBlock)(NSDictionary *returnDic, NSString *msg); typedef void(^FailureBlock)(NSString *errorInfo); typedef void(^LoadProgress)(float progress); @interface CBCacheHttpRequet : NSObject /** * Get请求 不对数据进行缓存 * * @param urlStr url * @param success 成功的回调 * @param failure 失败的回调 */ +(void)getRequestUrlStr:(NSString *)urlStr success:(SuccessBlock)success failure:(FailureBlock)failure; /** * Get请求 对数据进行缓存 * * @param urlStr url * @param success 成功的回调 * @param failure 失败的回调 */ +(void)getRequestCacheUrlStr:(NSString *)urlStr success:(SuccessBlock)success failure:(FailureBlock)failure ; /** * Post请求 不对数据进行缓存 * * @param urlStr url * @param parameters post参数 * @param success 成功的回调 * @param failure 失败的回调 */ +(void)postRequestUrlStr:(NSString *)urlStr withDic:(NSDictionary *)parameters success:(SuccessBlock )success failure:(FailureBlock)failure; /** * Post请求 对数据进行缓存 * * @param urlStr url * @param parameters post参数 * @param success 成功的回调 * @param failure 失败的回调 */ +(void)postRequestCacheUrlStr:(NSString *)urlStr withDic:(NSDictionary *)parameters success:(SuccessBlock )success failure:(FailureBlock)failure; /** * 上传单个文件 * * @param urlStr 服务器地址 * @param parameters 参数 * @param imageStr 上传的key * @param imageData 上传的问件 * @param loadProgress 上传的进度 * @param success 成功的回调 * @param failure 失败的回调 */ +(void)upLoadDataWithUrlStr:(NSString *)urlStr withDic:(NSDictionary *)parameters imageKey:(NSString *)imageStr withImageData:(NSData *)imageData upLoadProgress:(LoadProgress)loadProgress success:(SuccessBlock)success failure:(FailureBlock)failure; //清除接口数据缓存 +(void)deleteCache; @end
### 回答1: 设计一个基于 JavaWeb 的线上点餐系统需要考虑以下几个方面: 1. 用户界面设计:需要设计一个易用、直观、美观的用户界面,让用户可以轻松浏览菜单、点餐、结账等操作。 2. 数据库设计:需要设计数据库模型,用于存储菜单信息、用户信息、订单信息等数据。需要考虑到数据库的安全性和可扩展性。 3. 后端服务设计:需要设计后端服务,实现用户注册、登录、点餐、下单、支付等功能。后端服务需要考虑到系统的可靠性和安全性,可以采用 Spring、SpringMVC、MyBatis 等框架来实现。 4. 前端交互设计:需要设计前端页面,实现用户与系统的交互,可以采用 HTML、CSS、JavaScript 等技术来实现。 5. 系统架构设计:需要设计系统架构,包括系统的分层结构、模块划分、系统的部署方式等。 6. 安全性设计:需要考虑系统的安全性,包括用户数据的安全存储、交互数据的加密传输、用户身份验证等方面。 7. 性能优化设计:需要考虑系统的性能优化,包括数据的缓存、页面的静态化、数据库的优化等方面。 以上是设计基于 JavaWeb 的线上点餐系统需要考虑的几个方面,具体实现还需要根据具体的业务需求进行设计和开发。 ### 回答2: 设计一个基于JavaWeb的线上点餐系统,主要包括以下几个步骤: 1. 需求分析:确定系统的功能需求,包括用户注册登录、浏览菜单、下单支付、订单管理等基本功能。 2. 数据库设计:根据需求设计数据库表结构,包括用户表、菜品表、订单表等。使用MySQL等关系型数据库存储数据。 3. 后台开发:使用Java编写后台代码,包括处理用户请求、数据操作、业务逻辑等。使用Spring MVC等框架实现请求的解析与分发。 4. 前端开发:使用HTML、CSS、JavaScript等技术开发前端页面,包括用户注册登录界面、菜品展示界面、购物车界面等。 5. 集成支付接口:选择合适的支付接口集成,实现用户支付功能。 6. 流程优化:不断优化系统流程,提高用户体验。例如,可引入搜索功能、添加菜品分类、推荐菜品等。 7. 安全防护:加强系统的安全性,对用户的密码进行加密处理,确保用户信息的安全性。 8. 性能优化:对系统进行性能优化,减少响应时间,提高系统的并发能力。 9. 测试与部署:进行系统功能测试,修复可能存在的bug,并进行系统的部署与上线。 10. 维护与升级:定期检查系统运行情况,修复可能存在的问题,随时根据需求进行系统的升级与扩展。 通过以上步骤,一个基于JavaWeb的线上点餐系统就可以进行设计和开发。 ### 回答3: 设计一个基于JavaWeb的线上点餐系统需要考虑以下几个方面: 1. 数据库设计:需要设计用于存储菜品信息、订单信息、用户信息等的数据库。可以使用MySQL等数据库管理系统来存储数据。 2. 用户注册与登录:设计用户注册与登录功能,用户可以通过注册账户来登录系统,以便进行点餐和查看订单等操作。 3. 菜品展示与搜索:实现菜品展示页面,展示各个菜品的名称、价格、图片等信息,并提供搜索功能,方便用户根据关键词查找菜品。 4. 购物车功能:设计购物车功能,用户可以将想要点餐的菜品添加到购物车中,并在购物车中查看已选择的菜品和总价,还可以对菜品进行数量的增减操作。 5. 订单生成与支付:用户在购物车确认订单后,系统生成订单,并提供选择支付方式的功能。可以使用第三方支付接口(如支付宝、微信支付)来实现支付功能。 6. 订单管理:设计管理员端界面,管理员可以查看所有订单的详细信息,并对订单进行处理,如确认订单、取消订单等操作。 7. 用户评价与反馈:用户可以对点餐的菜品进行评价,并提供反馈意见,系统可以展示用户的评价和反馈信息。 8. 安全性设计:考虑用户信息的安全性,使用合适的加密算法对用户密码进行加密存储,并使用用户名和密码进行用户身份验证。 以上是基于JavaWeb的线上点餐系统的设计要点,当然在实际开发中还需要进行更加详细的设计和实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chengbo_eva

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值