背景 #
在开发过程中遇到一个很常见的场景,要求对当前页面的数据进行实时更新,很容易想到的就是开个定时器,每隔几秒钟查询一次,拿到最新的数据,再根据数据是否和当前数据相同,来决定是否触发渲染。
在实现过程中发现,由于一个页面使用的都是所谓的总线接口,也就是用不同参数请求同一个接口,导致同一时间会触发 n 次请求,造成了服务器压力突增。后端就来找前端解决问题了。
方案设计 #
方案一 #
手动控制每个请求的间隔,比如依次延时 1,2,3,4,5 秒,简单来说就是给定时器的延迟依次设置 1000,2000,3000…,延迟过后再去开启轮询
timerRef.current = setTimeout(() => {
intervalRef.current = setInterval(fetch, interval);
}, 1000);这种方案并不能彻底解决问题,因为请求的处理时间是不可控的,就会导致
- 当 1 秒延迟的请求,处理了 n 秒钟,那么就会和后面的请求重合,从而导致在一定时间内请求数增多的情况,但是如果重合的个数在允许的范围内还是可以这么做的。
- 当 1 秒延迟的请求,处理时间过长,下一个 1 秒延迟的请求又发出了,这种一直积攒的情况可能导致服务器崩溃。
方案二 #
使用队列的思维,实现类似红绿灯的控制效果,当前一个请求处理完了之后,再执行下一个请求,依次类推,保证在一段时间内只有一个请求在执行。这里使用 p-queue 去处理请求队列。
创建任务调度器
export function createScheduler(request, interval, queue) {
let timerId: number | undefined = undefined;
let active = false;
async function run() {
if (!active) return;
if (timerId) {
clearTimeout(timerId);
timerId = undefined;
}
try {
await queue?.add(request);
} catch (err) {
console.error("Task error:", err);
}
timerId = setTimeout(run, interval);
}
return {
start() {
if (active) return;
active = true;
timerId = setTimeout(run, interval);
},
stop() {
active = false;
clearTimeout(timerId);
},
};
}使用任务调度器
import PQueue from "p-queue";
const queue = new PQueue({ concurrency: 1 }); // 并发为1
const tasks = [
{
id: "Task 1",
request: async () => {
addLog("开始执行 Task 1");
await new Promise((r) => setTimeout(r, 5000)); // 添加一点延迟
addLog("完成执行 Task 1");
},
interval: 1000,
},
{
id: "Task 2",
request: async () => {
addLog("开始执行 Task 2");
await new Promise((r) => setTimeout(r, 0)); // 添加一点延迟
addLog("完成执行 Task 2");
},
interval: 1000,
},
];
useEffect(() => {
addLog("初始化调度器");
const schedulers = tasks?.map(({ request, interval, id }) =>
createScheduler(
() => {
addLog(`调度器触发: ${id}`);
return request();
},
interval,
queue,
id,
2000
)
);
schedulers?.forEach((scheduler) => scheduler?.start());
return () => {
schedulers?.forEach((scheduler) => scheduler?.stop());
addLog("停止所有调度器");
};
}, []);实现演示 #
后日谈 #
方案二对比方案一来说多了请求控制的特性,但是依然面临单个请求时间过长,阻塞后续请求的问题,这时候可以从两方面解决
- p-queue的并发改为2,保证在一个阻塞的情况下,其他的请求依旧能执行,除了时间长的请求外,其他请求依然受控,但如果有大部分请求时间都过长,那也不能无限增加并发数,这时候需要后端进行优化。
- 增加超时机制,超过一定时间,取消当前请求,可能存在的情况是一直超时,一直更新不了最新的数据,这时候加入重试机制,超时超过多少次之后就不在请求了。