一、故事的开始
晚上领导在工作群里发了一张华为云服务器的性能监控图:
二、主因
我们的系统在多人同时使用的情况下短时间内出现了服务器撑不住的情况,这是因为在监控的业务中前端轮询了一个性能很差的后端接口。
但在极端情况下,我认为前后端都有自己应该兜底的场景:
弱网环境下、用户频繁操作、大数据量
等情况下的页面展示前端需要做兜底
,不能出现不合理的表现,更不能接受页面崩溃。- 而面对
高并发
的请求,显然后端应该做兜底
,出现服务器崩溃是比较大的事故。
三、解决方案和分析
方案:
后端短时间内搞定不了,那只能前端在轮询方面做一下优化,目标是让上次的接口请求完成(fulfilled/rejected)后再进行下一次请求。
分析:
原来代码使用vueuse的useIntervalFn来做轮询,这个hook的好处是提供了现成的重启、停止的方法来控制定时器,另外不用担心页面销毁时定时器未被移除:
如果我做大幅度的改动和业务代码耦合在一起,风险比较大:
- 可能会忘记移除定时器;
- 可能不小心对原来的业务代码进行了删减;
- 耦合在业务中,命令式得实现这个需求会让代码上下跨度比较大,易导致逻辑混乱,且可维护性差。
尝试解决:
coding的思路是,封装一个和useIntervalFn用法一样的hook,但在动手之前我需要找一下vueuse是否有现成的,这样会节省时间,找了一圈没有。(我甚至后面还找vueuse的官方成员远方大佬确认了一下确实没有)
实现了一版:
最后完善:
经过单元测试发现,当快速连续执行resume时,会短时间内存在多个计时器,review代码发现当快速resume时,clean和fn先后同步执行,但是下一个timer要等待fn完成后才会被赋值,所以clean不能及时清除上一个计时器:
解决方法就是每次resume时记录最新的resumeId,finally的回调中只对最新的resumeId进行定时器的赋值:
另外还做了其他完善,比如hook的delay参数可以是一个getter或者ref。
最后,业务代码中的改动几乎就只是将useIntervalFn
改成useDelayIntervalFn
:
完整代码:
import { MaybeRefOrGetter, toValue } from "@vueuse/core";
import _ from "lodash";
import { isRef, onUnmounted, ref, watch } from "vue";
interface UseDelayIntervalFnOptions {
immediateCallback?: boolean;
}
export function useDelayIntervalFn(
fn: () => Promise<any>,
delay: MaybeRefOrGetter<number> = 1000,
options: UseDelayIntervalFnOptions
) {
const { immediateCallback = false } = options;
let timer: ReturnType<typeof setTimeout> | null = null;
const isActive = ref(false);
function clean() {
if (timer) {
clearInterval(timer);
timer = null;
}
}
function pause() {
isActive.value = false;
clean();
}
let resumeId = "";
let resume = () => {
let tempResumeId = _.uniqueId();
resumeId = tempResumeId;
const intervalValue = toValue(delay);
if (intervalValue <= 0) return;
isActive.value = true;
clean();
fn().finally(() => {
// 只执行最近的一次resume的回调
if (tempResumeId !== resumeId) return;
timer = setTimeout(() => {
if (!isActive.value) return;
resume();
}, intervalValue);
});
};
if (immediateCallback) {
resume();
}
let stopWatch = () => { };
if (isRef(delay) || typeof delay === "function") {
stopWatch = watch(delay, () => {
if (isActive.value) resume();
});
}
onUnmounted(() => {
pause();
stopWatch();
});
return {
isActive,
resume,
pause,
};
}
如发现代码实现有问题,欢迎指点!