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

本文介绍了如何使用Promise和状态管理来优化并发缓存,通过解决请求并发问题,确保了后续请求在第一次请求完成后再执行,提高了性能。作者还探讨了回调函数和Promise的结合应用,以实现更优雅的错误处理和通知机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、概述

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

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

2.1、问题

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

2.2、思考

有了!我们写个定时器就好了呀,比如我们可以给第二次请求加个定时器,定时器时间到了再去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)
})

img

img

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

2.3、优化

可是用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)
})

img

img

再试一下失败的时候

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)
    }
)

img

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)
    }
  )
}

四、最后

本人每篇文章都是一字一句码出来,希望对大家有所帮助,多提提意见。顺手来个三连击,点赞👍收藏💖关注✨,一起加油☕

<think>好的,我需要帮助用户设计一个支持并发且具有分布式幂等性的接口。首先,我得理解用户的需求。用户提到了高并发、分布式和幂等性这三个关键点。高并发意味着系统需要处理大量同时的请求,而分布式则涉及多个服务实例或节点之间的协作。幂等性则是确保同一请求多次执行的结果一致,防止重复操作带来的问题。 接下来,我需要回忆之前用户提供的参考资料。引用[1]提到接口幂等性和防重提交通常结合使用,以确保系统稳定和数据准确。引用[2]和[3]讨论了使用分布式锁,特别是通过Redis或Zookeeper实现,利用其互斥性来保证同一业务唯一标识的请求只能被处理一次。 首先,确定如何实现幂等性。常见的方案有唯一标识符(如请求ID)、数据库唯一索引、乐观锁、分布式锁和Token机制。用户需要高并发,所以方案必须高效,不能成为性能瓶颈。例如,数据库唯一索引可能在写入时产生锁,影响并发性能,而分布式锁如果设计不当也可能导致延迟。 然后考虑高并发下的性能优化。Redis作为内存数据库,处理速度快,适合用作分布式锁或Token存储。同时,异步处理可以将请求放入消息队列,削峰填谷,提高吞吐量。负载均衡和服务拆分也能分散压力,避免单点瓶颈。 接下来是分布式系统的设计要点。一致性哈希可以提升缓存的命中率和数据分布的均衡性。熔断降级和限流机制如令牌桶或漏桶算法,能防止系统过载。分库分表则解决数据库层面的扩展性问题,避免单表数据量过大。 最后,整合这些技术点,形成一个完整的方案。例如,客户端生成唯一请求ID,服务端通过Redis分布式锁检查是否已处理。若未处理,获取锁后执行业务逻辑,完成后释放锁。同时,结合异步队列处理请求,数据库使用唯一约束和乐观锁确保数据一致性。限流和熔断机制保护系统在高并发下稳定运行。 需要注意的是,分布式锁的key必须是业务唯一标识,且设置合理的过期时间防止死锁。另外,Token机制需要前端配合,每次操作前先获取Token,提交时携带Token进行验证,确保一次性有效。 可能的问题点包括分布式锁的性能开销、Redis的高可用性,以及如何保证Token机制的原子性操作。需要评估不同方案的适用场景,比如对于写操作密集的场景,乐观锁可能更合适;而对于读多写少的情况,分布式锁结合唯一索引可能更有效。 总结下来,设计并发分布式幂等接口需要综合运用多种技术,从请求标识、分布式锁、异步处理到数据库约束,同时结合系统层面的优化措施,确保高性能和高可用性。</think>### 高并发分布式幂等接口设计方案 #### 1. **幂等性实现核心方案** - **(1) 唯一请求标识** - 客户端生成全局唯一请求ID(如UUID、雪花算法),请求时携带该ID[^2]。 - 服务端通过Redis记录请求ID状态:`SET request_id 1 EX 60 NX`[^3]。 ```java // 伪代码示例:Redis校验唯一请求ID boolean isNewRequest = redis.setIfAbsent(requestId, "processing", 60); if (!isNewRequest) { throw new DuplicateRequestException(); } ``` - **(2) 分布式锁+业务唯一键** - 使用Redis/Zookeeper实现分布式锁,锁的key=业务主键(如订单号)。 - 加锁逻辑: ```python # 伪代码:Redis分布式锁(原子性操作) lock_key = f"lock:{biz_id}" if redis.set(lock_key, 1, ex=30, nx=True): try: process_request() finally: redis.delete(lock_key) else: return "操作已提交,请勿重复请求" ``` - **(3) 数据库层保障** - 唯一索引:对业务主键字段添加数据库唯一约束。 - 乐观锁:更新时基于版本号校验: ```sql UPDATE orders SET status = 'paid', version = version + 1 WHERE id = 1001 AND version = 3; ``` #### 2. **高并发优化策略** - **(1) 异步处理架构** ```mermaid graph LR A[客户端] --> B{API网关} B --> C[消息队列] C --> D[Worker集群] D --> E[(数据库)] ``` - 请求先写入Kafka/RocketMQ,通过削峰填谷提升吞吐量[^1]。 - **(2) Redis集群优化** - 采用Redis Cluster分片存储 - 读写分离+连接池配置 - Lua脚本保证原子性操作: ```lua -- 校验请求ID+扣减库存的原子操作 if redis.call('GET', KEYS[1]) == ARGV[1] then return nil else redis.call('SET', KEYS[1], ARGV[1], 'EX', 60) redis.call('DECR', 'stock_count') return 1 end ``` - **(3) 限流与降级** - 网关层限流:令牌桶算法控制QPS - 服务熔断:Hystrix/Sentinel实现故障隔离 #### 3. **完整流程示例** 1. 客户端生成请求ID=`req_123`,携带业务ID=`order_456` 2. 网关层校验参数合法性,触发限流规则 3. 服务端尝试获取Redis锁:`lock:order_456`(过期时间30秒) 4. 若获取成功: - 检查数据库幂等状态 - 执行业务逻辑 - 更新数据库并释放锁 5. 若获取失败: - 直接返回幂等响应 #### 4. **关键指标对比** | 方案 | 适用场景 | 性能影响 | 实现复杂度 | |--------------------|---------------|---------|-----------| | 数据库唯一索引 | 低频写操作 | 较高 | 低 | | Redis分布式锁 | 高频并发请求 | 中等 | 中 | | 令牌机制 | 前端交互强管控 | 低 | 高 | #### 5. **异常场景处理** - **网络超时重试**:客户端需区分幂等错误码(如409 Conflict) - **锁过期**:需设计锁续期机制(Redlock算法) - **数据一致性**:结合本地事务表+消息最终一致性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小马甲丫

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

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

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

打赏作者

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

抵扣说明:

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

余额充值