背景
前面写了网络请求的「频控」和「QPS」的文章:
可以想象,有这两种需求,肯定是有大量并发请求 的场景。除了控制请求的频率,还有一个思路也很有效果:去除冗余的重复请求。毫无疑问,要延续我们优雅的低侵入风格,Decorator 继续走起。
正文
思路
去除冗余的重复请求,最简单直接的思路就是把相同请求的结果缓存起来。下次发现请求相同,直接从内存读取结果返回,直接就不用发请求了。当然,还有一些细节需要处理,比如:
- 什么时候清除缓存?
- 如果前面的请求还没有返回,又发起了相同的请求怎么办?
- 如果返回报异常怎么处理?
- ......
这些点用语言解释都太苍白了,直接看代码,一眼就懂的事儿。但是,第 2 点是比较关键也有一定的难度,所以我们稍微展开说说。
现在主流的异步操作,都是使用的 Promise,我们这次也不例外。所以,如果前序请求 A1 还没有完成,重复的 A2 请求就发出了。我们只需要给 A2 返回 A1 的 Promise 就可以了。同理,A3、A4、A5......不管多少个请求来了,都是返回 A1 的 Promise。是不是听起来就很爽?!
什么?!还是不太明白?!
没关系,直接看代码。Talk is cheap, show me the code!
。
代码
ts
// 定义 requestCache 装饰器工厂函数
export const requestCache = (
originalFunction: (...args: any[]) => Promise<any>,
cacheDuration: number,
) => {
const cache: Map<string, any> = new Map();
const cachePromise: Map<string, Promise<any>> = new Map();
// 返回一个装饰器函数
return (...args: any[]) => {
const [url, payload] = args;
// 生成唯一的缓存键
const cacheKey = `${url}-${JSON.stringify(payload)}`;
// 检查是否有缓存
if (cache.has(cacheKey)) {
return Promise.resolve(cache.get(cacheKey));
}
// 检查是否有进行中的 Promise
if (!cachePromise.has(cacheKey)) {
cachePromise.set(
cacheKey,
originalFunction(...args)
.then((result) => {
// 请求完成后缓存结果
cache.set(cacheKey, result);
// 缓存过期后清除缓存,防止内存泄露
setTimeout(() => {
cache.delete(cacheKey);
}, cacheDuration);
return result;
})
.finally(() => {
// 请求结束后清除 Promise 缓存
cachePromise.delete(cacheKey);
}),
);
}
return cachePromise.get(cacheKey)!;
};
};
Useage:
ts
// 同时最多并发 5 个,每秒最多请求 9 个,5 秒内走缓存的请求
export const limitPost =
requestCache(
limitQps(
limitConcurrency(
request.post, 5),
9),
5000,
);
总结
以上就是利用缓存,解决重复请求的思路和代码。还是有一些不完善的地方,比如:
- 只针对 url 和 payload 进行了去重判定(因为实际项目中,通常只要 POST 请求需要控制);
- 如果有些需要重试的逻辑,接口返回的是正常的 200,没有走 catch,而是走了 then。那么错误的结果也是会被缓存的,重试的逻辑会受影响;
这些问题都需要根据项目的实际情况来改良。大家领会这其中的思路即可,相信这里绝大多数的代码是可以复用的。希望大家在追求极致的路上稳步前进。
PS: 本文代码用了 ChatGPT 生成基础代码,然后笔者进行了优化,确实可以提效。