跳转到主内容
Avatar
GrapeWell

轮询控制

2025-05-21

背景 #

在开发过程中遇到一个很常见的场景,要求对当前页面的数据进行实时更新,很容易想到的就是开个定时器,每隔几秒钟查询一次,拿到最新的数据,再根据数据是否和当前数据相同,来决定是否触发渲染。

在实现过程中发现,由于一个页面使用的都是所谓的总线接口,也就是用不同参数请求同一个接口,导致同一时间会触发 n 次请求,造成了服务器压力突增。后端就来找前端解决问题了。

方案设计 #

方案一 #

手动控制每个请求的间隔,比如依次延时 1,2,3,4,5 秒,简单来说就是给定时器的延迟依次设置 1000,2000,3000…,延迟过后再去开启轮询

timerRef.current = setTimeout(() => {
  intervalRef.current = setInterval(fetch, interval);
}, 1000);

这种方案并不能彻底解决问题,因为请求的处理时间是不可控的,就会导致

  1. 当 1 秒延迟的请求,处理了 n 秒钟,那么就会和后面的请求重合,从而导致在一定时间内请求数增多的情况,但是如果重合的个数在允许的范围内还是可以这么做的。
  2. 当 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("停止所有调度器");
  };
}, []);

实现演示 #

后日谈 #

方案二对比方案一来说多了请求控制的特性,但是依然面临单个请求时间过长,阻塞后续请求的问题,这时候可以从两方面解决

  1. p-queue的并发改为2,保证在一个阻塞的情况下,其他的请求依旧能执行,除了时间长的请求外,其他请求依然受控,但如果有大部分请求时间都过长,那也不能无限增加并发数,这时候需要后端进行优化。
  2. 增加超时机制,超过一定时间,取消当前请求,可能存在的情况是一直超时,一直更新不了最新的数据,这时候加入重试机制,超时超过多少次之后就不在请求了。