进阶篇代码使用的缓存淘汰策略是 FIFO (First-In, First-Out) ,因为它在缓存满时总是删除 this.pool.keys().next().value
,即最早插入 Map
的条目。
我们可以将其优化为 LRU (Least Recently Used - 最近最少使用) 策略。LRU 的核心思想是:当缓存满时,优先淘汰最长时间未被访问(读取或写入)的条目。
优化方法:利用 Map
的特性模拟 LRU
JavaScript 的 Map
会记住元素的插入顺序。我们可以利用这一点,在每次访问(get
或 set
)缓存时,将被访问的条目重新插入到 Map
中,这样它就会被移动到迭代顺序的末尾,代表"最近使用"。当需要淘汰时,我们仍然删除迭代顺序的第一个元素 (keys().next().value
),此时它代表的就是"最久未使用"的条目。
修改后的代码:
javascript
// 这里我引入的是axios已经创建的实例,这样可以和现有项目完美结合
import { axiosIns } from "./httpService.js";
import { stringify } from "qs";
import CryptoJS from "crypto-js";
class AdvancedRequestPool {
constructor(options = {}) {
this.pool = new Map(); // 请求池,用于存储请求的键值对 (现在实现 LRU)
this.defaultTTL = options.defaultTTL || 5000; // 默认缓存时间(毫秒)
this.maxSize = options.maxSize || 100; // 请求池的最大大小
this.encryptKey = options.encryptKey || null; // 可选的加密密钥
this.abortControllers = new Map(); // 存储 AbortController 引用
this.retryRules =
options.retryRules || AdvancedRequestPool.defaultRetryRules;
this.enableRegularCleaning = options.enableRegularCleaning || false;
if (this.enableRegularCleaning)
this.cleanupInterval = setInterval(this.cleanup.bind(this), 60000);
}
// --- 其他方法保持不变 ---
static defaultRetryRules = {
networkErrors: true,
httpStatus: [500, 502, 503, 504, 429, 408],
customCheck: null,
};
isAbortError(error) {
return (
error.name === "AbortError" ||
(error.response && error.response.status === 499)
);
}
isRetryableError(error) {
if (this.isAbortError(error)) return false;
if (this.retryRules.customCheck) {
return this.retryRules.customCheck(error);
}
if (this.retryRules.networkErrors && !error.response) return true;
if (error.response) {
const status = error.response.status;
return this.retryRules.httpStatus.includes(status);
}
return false;
}
generateKey(config) {
const { method, url, params, data } = config;
const sortedParams = stringify(params, {
sort: (a, b) => a.localeCompare(b),
});
const sortedData =
typeof data === "object"
? stringify(data, { sort: (a, b) => a.localeCompare(b) })
: data;
const rawKey = `${method}-${url}-${sortedParams}-${sortedData}`;
return this.encryptKey
? CryptoJS.HmacSHA256(rawKey, this.encryptKey).toString()
: rawKey;
}
// 发送请求 - 重点修改部分
async request(config) {
const key = this.generateKey(config);
const now = Date.now();
// --- LRU 优化点 1: 缓存命中时更新其位置 ---
if (this.pool.has(key)) {
const entry = this.pool.get(key);
if (entry.expire > now) {
// --- LRU 核心: 将命中的条目移到末尾 (最近使用) ---
this.pool.delete(key); // 先删除
this.pool.set(key, entry); // 再添加,使其成为最新的
// --- LRU 核心结束 ---
console.log(`Cache hit for key: ${key}`); // 可选:日志记录
return entry.promise; // 返回缓存的 Promise
} else {
// 缓存存在但已过期
console.log(`Cache expired for key: ${key}`); // 可选:日志记录
entry.promise.abort?.(); // 中止旧的请求(如果还在进行中)
this.pool.delete(key); // 从缓存池中删除过期的条目
this.abortControllers.delete(key); // 也删除对应的 AbortController
}
}
// --- 缓存未命中 或 缓存已过期 ---
console.log(`Cache miss or expired for key: ${key}`); // 可选:日志记录
// --- LRU 优化点 2: 缓存淘汰逻辑 (淘汰最久未使用的) ---
// 在添加新条目 *之前* 检查大小,如果需要则淘汰
if (this.pool.size >= this.maxSize) {
// Map 的 keys().next().value 返回的是最早插入的键
// 因为我们在命中时会重新插入,所以最早插入的 == 最久未使用的
const lruKey = this.pool.keys().next().value;
console.log(`Cache full, evicting LRU key: ${lruKey}`); // 可选:日志记录
const lruEntry = this.pool.get(lruKey);
lruEntry?.promise.abort?.(); // 尝试中止被淘汰的请求
this.pool.delete(lruKey); // 从缓存池淘汰
this.abortControllers.delete(lruKey); // 从控制器映射中淘汰
}
// --- LRU 淘汰逻辑结束 ---
// --- 创建和管理 AbortController ---
let controller = this.abortControllers.get(key);
// 如果因为过期或其他原因已被删除,则重新创建
if (!controller) {
controller = new AbortController();
this.abortControllers.set(key, controller);
}
// --- AbortController 管理结束 ---
const finalConfig = {
...config,
signal: controller.signal,
};
const promise = axiosIns(finalConfig)
.then((response) => {
// 成功后,如果配置了自动刷新,安排刷新
if (config.autoRefresh) {
this.scheduleRefresh(key, config);
}
return response;
})
.catch((error) => {
// 处理重试逻辑
// 注意:使用请求配置中的 retryCount,并且需要递减
const currentRetryCount = config.retryCount ?? 0;
if (currentRetryCount > 0 && this.isRetryableError(error)) {
// 创建一个包含递减后重试次数的新配置对象,用于下一次重试
const configForNextRetry = {
...config, // 保持原始配置的其他部分
retryCount: currentRetryCount - 1 // 递减重试次数
};
return this.handleRetry(key, configForNextRetry, error); // 传递递减后的配置
}
// 如果没有重试次数或错误不可重试,则抛出错误
// console.error(`Request failed permanently for key ${key} after exhausting retries or due to non-retryable error.`, error);
throw error; // 抛出错误,终止 Promise 链
})
.finally(() => {
// 如果请求不需要保持活动状态,则在完成后清理
// 注意:如果启用了缓存 TTL,这里不应该删除,让 cleanup 或下次访问时处理
if (!config.keepAlive && !(config.cacheTTL > 0 || this.defaultTTL > 0)) {
this.pool.delete(key);
this.abortControllers.delete(key);
} else if (!this.pool.has(key) && this.abortControllers.has(key)) {
// 特殊情况:如果在finally之前被abort了,确保控制器也被清理
// console.log(`Cleaning up orphan AbortController for key: ${key}`);
this.abortControllers.delete(key);
}
});
// 为 Promise 添加 abort 方法
promise.abort = () => {
controller.abort();
// abort 时也应该清理缓存和控制器引用
this.pool.delete(key);
this.abortControllers.delete(key);
console.log(`Request aborted for key: ${key}`); // 可选:日志记录
};
// 将新的请求 Promise 存入缓存池
// --- LRU 优化点 3: 新条目添加到末尾 ---
this.pool.set(key, {
promise,
expire: Date.now() + (config.cacheTTL || this.defaultTTL),
config, // 存储本次请求的配置
});
// 新条目自然被添加到 Map 的末尾,成为"最新"的
// --- LRU 添加逻辑结束 ---
return promise;
}
// 清理过期的请求 - 无需改变
cleanup() {
const now = Date.now();
let cleanedCount = 0; // 可选:计数
this.pool.forEach((value, key) => {
if (value.expire < now) {
value.promise.abort?.();
this.pool.delete(key);
this.abortControllers.delete(key);
cleanedCount++;
}
});
if (cleanedCount > 0) {
console.log(`Cleaned up ${cleanedCount} expired cache entries.`); // 可选:日志
}
}
// 安排自动刷新 - 无需改变,但注意它会重新调用 request,从而更新 LRU 顺序
scheduleRefresh(key, config) {
// 在安排前检查缓存是否仍然存在,避免不必要的定时器
if (this.pool.has(key)) {
const refreshInterval = config.refreshInterval || 30000;
console.log(`Scheduling refresh for key: ${key} in ${refreshInterval}ms`); // 可选:日志
setTimeout(() => {
// 再次检查,可能在等待期间被删除了
if (this.pool.has(key)) {
console.log(`Refreshing request for key: ${key}`); // 可选:日志
// 注意:旧的 promise 不需要手动 abort,因为 request 方法内部
// 在覆盖缓存前会检查并 abort 过期或需要替换的 promise
this.request({ ...config, keepAlive: true }); // 重新请求,标记为 keepAlive
}
}, refreshInterval);
}
}
// 处理重试逻辑 - 稍作调整以接收递减后的配置
async handleRetry(key, configWithDecrementedRetry, error) {
console.log(`Attempting retry for key: ${key}. Retries left: ${configWithDecrementedRetry.retryCount}`); // 可选:日志
// 无需再次检查 isRetryableError,因为调用前已经检查过了
return new Promise((resolve, reject) => {
setTimeout(async () => {
try {
// 使用带有递减重试次数的配置重新发起请求
const retryPromise = this.request(configWithDecrementedRetry);
// --- 更新缓存中的 Promise ---
// 检查缓存条目是否仍然存在(可能在延迟期间被清理或中止)
if (this.pool.has(key)) {
const entry = this.pool.get(key);
// 只有当缓存中的 Promise 还是原来的那个失败的 Promise 时才更新
// (虽然理论上不太可能,但以防万一)
// 注意:这里比较 Promise 引用可能不可靠,更稳妥的方式是直接更新
this.pool.set(key, {
...entry, // 保留 expire 和 config
promise: retryPromise, // 更新为重试的 Promise
});
}
// --- 更新缓存结束 ---
resolve(await retryPromise);
} catch (retryError) {
// 如果重试仍然失败
console.error(`Retry failed for key: ${key}`, retryError); // 可选:日志
// 将最终的错误 reject 出去,传递给调用者
reject(retryError);
}
}, configWithDecrementedRetry.retryDelay || 1000);
});
}
// 添加全局中止方法 - 无需改变
abortRequest(config) {
const key = this.generateKey(config);
const controller = this.abortControllers.get(key);
if (controller) {
console.log(`Aborting request via abortRequest for key: ${key}`); // 可选:日志
controller.abort();
// Abort 时也应该清理缓存池
this.pool.delete(key);
this.abortControllers.delete(key); // 确保控制器也被移除
}
}
// 销毁实例,清理所有资源 - 无需改变
destroy() {
if (this.enableRegularCleaning) clearInterval(this.cleanupInterval);
this.pool.forEach((value, key) => {
value.promise.abort?.();
this.abortControllers.delete(key); // 确保清理控制器
});
this.pool.clear();
this.abortControllers.clear(); // 再次确保清空
console.log("AdvancedRequestPool instance destroyed."); // 可选:日志
}
}
// --- 实例化和导出 ---
const httpPool = new AdvancedRequestPool({
// 可以在这里配置 maxSize, defaultTTL, retryRules 等
maxSize: 50, // 例如,设置最大缓存为 50
defaultTTL: 1000 * 60 * 5, // 例如,默认缓存 5 分钟
enableRegularCleaning: true, // 启用定时清理
});
export default AdvancedRequestPool; // 导出类本身
export { httpPool }; // 导出默认实例
主要修改点解释:
-
缓存命中 (
request
方法内):- 当找到有效缓存 (
this.pool.has(key)
且未过期) 时: - 先
this.pool.delete(key)
删除该条目。 - 再
this.pool.set(key, entry)
将其重新添加。 - 这利用了
Map
按插入顺序迭代的特性,将刚访问的条目移动到了内部顺序的末尾,标记为"最近使用"。
- 当找到有效缓存 (
-
缓存淘汰 (
request
方法内):- 检查
this.pool.size >= this.maxSize
的逻辑不变。 - 获取
const lruKey = this.pool.keys().next().value;
也不变。 - 但此时,由于命中时会将元素移到末尾,所以
keys().next().value
获取到的实际上是最久未被访问(读取或添加/更新)的键,即 LRU 键。 - 删除
lruKey
对应的条目、中止其 Promise、删除其 AbortController。
- 检查
-
添加新条目 (
request
方法内):- 执行
this.pool.set(key, ...)
时,新条目会被自动添加到Map
的末尾,自然成为"最新"的条目。
- 执行
-
处理过期 (
request
方法内):- 当发现缓存存在但已过期时,明确地从
pool
和abortControllers
中删除该条目,避免它作为"最久未使用"的项被错误地保留在Map
的开头。
- 当发现缓存存在但已过期时,明确地从
-
重试逻辑 (
handleRetry
):- 调整了
handleRetry
的调用方式,传递递减后的重试次数配置。 - 在
handleRetry
内部重新发起this.request
时,如果请求成功,这个request
调用会像正常添加新条目一样,将成功的 Promise 放入pool
,并更新其 LRU 位置。 - 如果重试的
this.request
命中了缓存(理论上不太可能,因为是刚失败的请求,但逻辑上要考虑),也会更新 LRU 位置。 - 添加了在
handleRetry
内部更新pool
中 Promise 的逻辑,确保缓存持有的是最新的重试 Promise。
- 调整了
-
清理和中止:
- 确保在
abort
,cleanup
,destroy
, 以及淘汰逻辑中,都同时清理this.pool
和this.abortControllers
中的对应条目。
- 确保在
-
日志: 添加了一些
console.log
来帮助追踪缓存命中、未命中、过期、淘汰和重试等行为,方便调试。上线时可以移除或使用更完善的日志库。
现在,AdvancedRequestPool
的缓存淘汰策略就从简单的 FIFO 变成了更常用的 LRU。这通常能提供更好的缓存命中率,因为它会保留那些最近被频繁访问的数据。