面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:
yunmz777
。
并发请求来不及写数据库怎么办
在并发请求时,如果写数据库操作来不及执行,可以考虑以下几种解决方案:
1. 使用队列(Queue)
将所有的数据库写入操作放入队列中,按顺序依次处理,避免数据库操作被并发请求压垮。
解决方案:
-
将写入数据库的请求加入到队列中。
-
每次从队列中取出一个请求,执行数据库操作。
-
使用 异步任务 或 队列系统 (如 RabbitMQ 、Redis Queue 等)来确保数据库操作按顺序执行。
2. 批量处理
将多个数据库操作合并成一个批量操作进行写入,减少数据库写入次数,从而提升性能。
解决方案:
-
将多个并发请求的数据收集到一起,在适当的时机批量插入数据库。
-
可以使用 事务 来保证批量写入的原子性。
3. 数据库写入优化
通过优化数据库的写入性能,减少每次写入的延迟。可以使用以下方法:
-
数据库索引优化:避免频繁更新索引,提高写入速度。
-
异步写入:如果数据库操作不要求立即生效,可以将写入操作异步化,允许系统继续处理其他请求。
-
缓存写入:可以将数据缓存到内存(如 Redis),然后定期或在后台批量写入数据库。
4. 分布式锁
如果并发请求对同一资源进行修改,可以使用 分布式锁 (如 RedLock)来确保只有一个请求在某个时刻能写入数据库,避免竞态条件。
解决方案:
- 在操作数据库时,使用分布式锁,保证每次只有一个请求能够访问数据库写入操作。
总结:
-
使用 队列 和 批量处理 解决并发写入问题。
-
通过 数据库写入优化 和 异步操作 提升性能。
-
如果涉及同一资源,可以使用 分布式锁 来确保写操作不会冲突。
客户跟你抱怨网页太慢,你怎么用客户电脑定位问题?
当客户抱怨网页太慢时,你需要用客户的电脑进行排查,找出具体的性能瓶颈。你可以按照以下步骤进行分析:
1. 了解具体问题
首先,和客户沟通,获取更多细节:
-
网页是整体慢,还是特定页面或功能慢?
-
是在所有设备和网络环境下都慢,还是只在客户的电脑上慢?
-
是首次加载慢,还是每次都慢?
-
具体慢在哪里?是页面打开慢、加载资源慢、交互延迟,还是某些按钮点击无响应?
这些信息能帮助你缩小排查范围。
2. 用开发者工具(F12)进行性能分析
在客户的浏览器(Chrome、Edge、Firefox)中打开 开发者工具 (按 F12
或 Ctrl + Shift + I
),然后检查以下几个关键点:
(1)网络(Network)面板
-
让客户刷新页面(
Ctrl + R
或F5
)。 -
查看 "DOMContentLoaded" 和 "Load" 时间:
-
DOMContentLoaded
代表 HTML 和同步 JavaScript 解析完成。 -
Load
代表所有资源(图片、CSS、JS 等)加载完成。 -
如果 "DOMContentLoaded" 非常慢,说明 HTML 或者 JavaScript 解析有问题。
-
如果 "Load" 时间很长,可能是资源加载过多、CDN 问题、服务器响应慢等。
-
-
检查加载资源
-
找出 加载时间最长的资源,可能是:
-
某个图片/视频太大,导致加载过慢。
-
某个 API 请求慢,导致数据获取延迟。
-
某个 JS 或 CSS 资源加载太久,影响渲染。
-
-
-
检查是否有失败的请求
- 404、500、504 错误可能影响页面加载速度。
(2)性能(Performance)面板
-
录制页面加载情况
-
点击
Record
,然后刷新页面。 -
查看 主线程(Main Thread) 是否有长时间的任务阻塞页面交互。
-
-
检查渲染阻塞
- 过多的 JavaScript 执行可能会导致渲染卡顿。
(3)JavaScript Profiler
-
在
Performance
里检查 脚本执行是否占用过多时间。 -
过多的 JS 可能会导致浏览器 主线程阻塞,影响页面交互流畅度。
3. 运行 Chrome Lighthouse 进行优化分析
-
在
开发者工具 -> Lighthouse
选项卡中,选择Performance
进行评测。 -
分析得分,看看是否有:
-
服务器响应时间过长(TTFB 高)。
-
图片优化不足(未使用 WebP)。
-
过大的 JS 或 CSS 资源(未压缩)。
-
阻塞渲染的资源(JS、CSS 影响首屏渲染)。
-
4. 检查客户电脑和网络
(1)检查网络速度
- 让客户 测试网络 (如
speedtest.net
),检查:- 带宽是否够快?
- 延迟是否过高?
- 是否是 WiFi 连接问题?
- 是否有其他设备占用大量带宽?
(2)检查 DNS 解析
-
让客户
ping
网站:shping example.com
如果延迟高,可能是 DNS 解析慢 ,建议换 Google DNS(8.8.8.8) 或 Cloudflare DNS(1.1.1.1)。
-
也可以
nslookup
或tracert
进行进一步排查:shnslookup example.com tracert example.com
(3)检查 CPU、内存、磁盘
- 如果客户电脑很慢 ,可能是 CPU 或 内存使用率过高:
- 打开
任务管理器(Ctrl + Shift + Esc)
。 - 查看 CPU、内存、磁盘占用,是否有异常进程。
- 关闭不必要的后台程序。
- 打开
5. 服务器端排查
如果本地检查没问题,可能是服务器问题:
- 服务器响应时间(TTFB)是否过高?
- CDN 访问是否正常?
- API 响应时间是否太长?
- 数据库查询是否耗时过长?
可以让客户在终端运行:
sh
curl -o /dev/null -s -w "%{time_total}\n" https://example.com
看看总响应时间是否过长。
总结
如果客户电脑网页加载慢,应该按照以下步骤排查:
-
和客户确认具体问题(网页整体慢?交互慢?资源加载慢?)
-
使用 F12 开发者工具分析
Network
面板检查 资源加载Performance
面板检查 渲染阻塞Lighthouse
评测优化建议
-
检查客户网络
speedtest
进行网络测速ping
检测延迟nslookup
和tracert
追踪域名解析
-
检查客户电脑
- 任务管理器查看 CPU、内存、磁盘占用
-
服务器端检查
-
服务器响应是否正常
-
API 响应时间是否过长
-
是否有数据库性能问题
-
通过这个流程,能有效定位问题并给客户提供优化方案!
控制请求并发的数量,例如有8个请求需要发送,如何控制同时只能并行发送两个请求
控制请求并发数量,确保一次只能并行发送指定数量的请求(例如同时只并行发送两个请求),可以使用多种方法。这里有几种常见的解决方案:
1. 使用 Promise 和 async/await 控制并发
我们可以使用 async/await
和 Promise
来控制并发请求的数量,使用 for
循环 或 map
来逐步发送请求,并确保在每次请求完成后再开始下一个请求。
示例代码:
javascript
async function sendRequests(requests, maxConcurrency) {
let index = 0;
// 控制并发请求的函数
async function handleRequest() {
if (index >= requests.length) return;
const currentIndex = index++; // 获取当前请求索引
await requests[currentIndex](); // 等待当前请求完成
handleRequest(); // 发起下一个请求
}
// 初始化最大并发请求数量
const concurrencyPromises = Array.from(
{ length: maxConcurrency },
handleRequest
);
await Promise.all(concurrencyPromises); // 等待所有并发请求完成
}
// 示例:模拟 8 个请求,最多并发 2 个
const requests = Array.from(
{ length: 8 },
(_, i) => () =>
new Promise((resolve) => {
console.log(`Request ${i + 1} started`);
setTimeout(() => {
console.log(`Request ${i + 1} finished`);
resolve();
}, Math.random() * 2000); // 随机延时模拟请求
})
);
sendRequests(requests, 2);
解释:
requests
是一个包含 8 个请求的数组,每个请求用一个返回Promise
的函数表示。maxConcurrency
控制并发请求的数量(本例中是 2)。- 使用
handleRequest
函数逐个处理请求,确保同一时间内只会有最多 2 个请求在并发执行。
2. 使用队列控制并发
通过使用一个 队列 来控制请求的发送。每次发送请求时,从队列中取出一个任务并执行,同时控制并发数量。
示例代码:
javascript
function controlConcurrency(requests, maxConcurrency) {
const queue = [...requests]; // 请求队列
let running = 0; // 当前并发数
const processQueue = async () => {
while (queue.length > 0 && running < maxConcurrency) {
const request = queue.shift(); // 从队列中取出请求
running++;
await request(); // 执行请求
running--;
processQueue(); // 递归调用,继续处理队列中的请求
}
};
processQueue(); // 初始化请求队列处理
}
// 示例:模拟 8 个请求,最多并发 2 个
const requests = Array.from(
{ length: 8 },
(_, i) => () =>
new Promise((resolve) => {
console.log(`Request ${i + 1} started`);
setTimeout(() => {
console.log(`Request ${i + 1} finished`);
resolve();
}, Math.random() * 2000); // 随机延时模拟请求
})
);
controlConcurrency(requests, 2);
解释:
queue
存储所有待请求的任务,每次从队列中取出一个任务并执行。running
控制当前正在执行的请求数量,确保并发数不会超过maxConcurrency
。- 当并发数低于最大限制时,自动从队列中获取新的任务并执行。
3. 使用第三方库(如 p-limit
)
使用第三方库,如 p-limit,可以方便地控制并发数量,限制同时运行的 Promise 数量。
示例代码:
bash
npm install p-limit
javascript
const pLimit = require("p-limit");
const limit = pLimit(2); // 设置最大并发数量为 2
const requests = Array.from(
{ length: 8 },
(_, i) => () =>
new Promise((resolve) => {
console.log(`Request ${i + 1} started`);
setTimeout(() => {
console.log(`Request ${i + 1} finished`);
resolve();
}, Math.random() * 2000); // 随机延时模拟请求
})
);
// 使用 p-limit 限制并发
const limitedRequests = requests.map((request) => limit(request));
Promise.all(limitedRequests).then(() => console.log("All requests completed"));
解释:
p-limit
库让你可以创建一个限制并发数的函数。每个请求都被包装在limit()
中,最大并发数为 2。- 使用
Promise.all()
来等待所有请求完成。
总结:
- 使用
async/await
和Promise
:逐个发起请求,确保每次只有maxConcurrency
个并发请求。 - 使用队列:通过队列来管理请求,确保在并发数限制下按顺序发起请求。
- 使用第三方库(如
p-limit
):使用库来简化并发控制,限制同时运行的 Promise 数量。
这些方法都能有效控制并发请求的数量,避免一次性发送过多请求导致性能问题。根据你的需求选择最适合的方式。
埋点mdn还没加载出来,怎么对页面之前的操作进行一个记录?
如果在页面加载过程中,埋点的 MDN 相关脚本(如 JavaScript 或分析工具)尚未加载完成,你可以通过以下几种方式来对页面之前的操作进行记录:
1. 使用本地存储(LocalStorage 或 SessionStorage)
在 MDN 脚本加载之前,可以将用户的操作记录到 localStorage
或 sessionStorage
中,等到 MDN 脚本加载完成后再将这些记录发送到服务器或分析工具。
方案:
- 在用户执行的操作时,记录到
localStorage
或sessionStorage
。 - 等 MDN 脚本加载完成后,再从存储中获取数据并进行提交。
示例:
javascript
// 用户操作时记录到 sessionStorage
function trackAction(action) {
const actions = JSON.parse(sessionStorage.getItem("actions")) || [];
actions.push(action);
sessionStorage.setItem("actions", JSON.stringify(actions));
}
// MDN 脚本加载完成后处理操作记录
function processStoredActions() {
const actions = JSON.parse(sessionStorage.getItem("actions"));
if (actions) {
actions.forEach((action) => {
// 向埋点工具发送数据
sendToAnalytics(action);
});
}
sessionStorage.removeItem("actions");
}
// 模拟发送数据到分析工具
function sendToAnalytics(action) {
console.log("Sending action to analytics:", action);
}
// 示例:用户点击按钮
document.getElementById("btn").addEventListener("click", function () {
trackAction("Button Clicked");
});
// 假设 MDN 脚本加载完毕时,调用 processStoredActions
window.addEventListener("load", processStoredActions);
2. 使用队列(Queue)
在 MDN 脚本加载前,将所有操作放入一个队列中,等脚本加载完成后再统一发送请求。
方案:
- 将用户的操作推入一个队列中。
- 等 MDN 脚本加载完成后,遍历队列并发送操作记录。
示例:
javascript
// 创建一个操作队列
const actionQueue = [];
// 用户操作时推送到队列
function trackAction(action) {
actionQueue.push(action);
}
// MDN 脚本加载完成后处理操作队列
function processQueue() {
actionQueue.forEach((action) => {
// 向埋点工具发送数据
sendToAnalytics(action);
});
actionQueue.length = 0; // 清空队列
}
// 模拟发送数据到分析工具
function sendToAnalytics(action) {
console.log("Sending action to analytics:", action);
}
// 示例:用户点击按钮
document.getElementById("btn").addEventListener("click", function () {
trackAction("Button Clicked");
});
// MDN 脚本加载完毕时,调用 processQueue
window.addEventListener("load", processQueue);
3. 使用 setTimeout
或 setInterval
检测 MDN 脚本加载
你可以定期检查 MDN 埋点脚本是否已经加载完成,加载完成后立即发送存储的操作记录。
方案:
- 使用
setTimeout
或setInterval
定期检查 MDN 脚本是否已加载。 - 如果加载完成,将之前的操作记录发送到 MDN。
示例:
javascript
let mdnLoaded = false;
const actionQueue = [];
// 检查 MDN 脚本是否加载
function checkMDNScript() {
if (typeof window.mdnAnalytics !== "undefined") {
// 假设 mdnAnalytics 是埋点工具的对象
mdnLoaded = true;
processQueue();
}
}
// 用户操作时推送到队列
function trackAction(action) {
actionQueue.push(action);
}
// MDN 脚本加载完毕后处理操作队列
function processQueue() {
if (mdnLoaded) {
actionQueue.forEach((action) => {
sendToAnalytics(action);
});
actionQueue.length = 0; // 清空队列
} else {
setTimeout(checkMDNScript, 100); // 每100ms检测一次
}
}
// 模拟发送数据到分析工具
function sendToAnalytics(action) {
console.log("Sending action to analytics:", action);
}
// 示例:用户点击按钮
document.getElementById("btn").addEventListener("click", function () {
trackAction("Button Clicked");
});
// 初始化检测 MDN 脚本
checkMDNScript();
4. 将用户操作缓存在全局变量中
可以将用户的操作存储在全局变量中,并在 MDN 脚本加载完成后处理这些数据。
方案:
- 定义一个全局变量来存储用户的操作。
- 在 MDN 脚本加载后,遍历全局变量中的操作记录,发送数据。
示例:
javascript
// 存储用户操作
window.userActions = [];
// 用户操作时记录到全局变量
function trackAction(action) {
window.userActions.push(action);
}
// MDN 脚本加载完成后处理操作记录
function processUserActions() {
if (window.userActions && window.userActions.length > 0) {
window.userActions.forEach((action) => {
sendToAnalytics(action);
});
window.userActions = []; // 清空记录
}
}
// 模拟发送数据到分析工具
function sendToAnalytics(action) {
console.log("Sending action to analytics:", action);
}
// 示例:用户点击按钮
document.getElementById("btn").addEventListener("click", function () {
trackAction("Button Clicked");
});
// 模拟MDN脚本加载完成时调用
window.addEventListener("load", processUserActions);
总结:
- 队列/缓存:在 MDN 脚本加载前将用户操作存储起来,脚本加载完成后再发送数据。
- 检查加载状态 :使用
setInterval
或setTimeout
监控 MDN 脚本加载状态,确保脚本加载完成后处理缓存的请求。
这些方法都能确保在 MDN 脚本加载完成之前,用户的操作不会丢失,并能够及时进行埋点记录。
秒杀按钮倒计时如何更精准
为了确保 秒杀按钮倒计时 更加精准,避免由于 JavaScript 执行延迟等问题引起的误差,通常的做法是基于 服务器时间 来计算倒计时。下面是一个完整的实现思路以及具体的代码示例。
实现思路:
- 获取服务器时间:服务器时间比本地时间更为准确,因此应该在前端获取服务器的时间戳,并根据服务器时间计算倒计时。
- 前端倒计时计算:根据服务器返回的时间戳,计算当前时间与目标时间(如秒杀结束时间)之间的差异,并更新前端显示。
- 同步更新:每秒钟更新时间,确保倒计时更新实时并且精确。
具体实现步骤:
- 获取服务器时间戳 :
- 通过向服务器发送请求,获取当前的服务器时间戳。
- 计算倒计时 :
- 计算当前时间与活动结束时间的差值,并显示剩余时间。
- 更新倒计时 :
- 每秒更新一次倒计时,并根据剩余时间动态更新按钮状态(比如显示 "即将开始" 或 "已结束")。
完整的示例代码:
前端代码:
html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>秒杀倒计时</title>
<style>
.countdown {
font-size: 20px;
font-weight: bold;
}
.btn {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
.disabled {
background-color: #ccc;
cursor: not-allowed;
}
.active {
background-color: #ff4d4f;
color: white;
}
</style>
</head>
<body>
<h1>秒杀倒计时</h1>
<p>剩余时间:<span id="countdown" class="countdown"></span></p>
<button id="seckillBtn" class="btn disabled" disabled>立即抢购</button>
<script>
// 目标秒杀结束时间(假设目标时间是2023年12月31日 23:59:59)
const endTime = new Date("2023-12-31T23:59:59").getTime();
// 获取服务器时间
async function getServerTime() {
try {
const response = await fetch("/api/server-time"); // 替换为实际的服务器 API 地址
const data = await response.json();
return data.timestamp * 1000; // 服务器时间返回的是秒,需要转换为毫秒
} catch (error) {
console.error("获取服务器时间失败:", error);
return Date.now(); // 如果失败,使用本地时间
}
}
// 更新倒计时
function updateCountdown(serverTime) {
const currentTime = serverTime || Date.now();
const remainingTime = endTime - currentTime;
if (remainingTime <= 0) {
// 秒杀结束
document.getElementById("countdown").textContent = "秒杀结束";
document.getElementById("seckillBtn").classList.add("disabled");
document.getElementById("seckillBtn").disabled = true;
} else {
// 计算剩余时间(天、小时、分钟、秒)
const days = Math.floor(remainingTime / (1000 * 60 * 60 * 24));
const hours = Math.floor(
(remainingTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
);
const minutes = Math.floor(
(remainingTime % (1000 * 60 * 60)) / (1000 * 60)
);
const seconds = Math.floor((remainingTime % (1000 * 60)) / 1000);
document.getElementById(
"countdown"
).textContent = `${days}天 ${hours}小时 ${minutes}分钟 ${seconds}秒`;
// 每秒更新倒计时
setTimeout(() => updateCountdown(serverTime + 1000), 1000);
}
}
// 启动倒计时
async function startCountdown() {
const serverTime = await getServerTime();
updateCountdown(serverTime);
}
// 启动倒计时
startCountdown();
// 启动秒杀按钮(例如:秒杀开始时启用)
function enableSeckillButton() {
document.getElementById("seckillBtn").classList.remove("disabled");
document.getElementById("seckillBtn").disabled = false;
}
// 假设秒杀在倒计时结束后立即启动
setTimeout(enableSeckillButton, endTime - Date.now());
</script>
</body>
</html>
解释:
-
服务器时间获取:
- 我们通过
fetch
向服务器发送请求,获取服务器当前的时间戳。服务器时间比客户端时间更准确,因此我们根据服务器时间来计算倒计时。 - 服务器返回的时间戳是以秒为单位的,所以在前端需要乘以
1000
转换为毫秒。
- 我们通过
-
倒计时计算:
- 计算从当前时间到目标结束时间之间的剩余时间。
- 根据剩余时间,我们计算出 天数、小时数、分钟数、秒数,并更新显示。
-
更新按钮状态:
- 在倒计时结束后,我们禁用秒杀按钮,并将按钮的文本更改为 "秒杀结束"。
- 在秒杀开始时,我们启用秒杀按钮,并将其文本设置为 "立即抢购"。
-
倒计时更新:
- 每秒钟更新一次倒计时,保证显示实时的剩余时间。通过
setTimeout
每秒调用一次更新函数。
- 每秒钟更新一次倒计时,保证显示实时的剩余时间。通过
-
模拟秒杀开始:
- 在倒计时结束后,通过
setTimeout
启用秒杀按钮,模拟秒杀开始的场景。
- 在倒计时结束后,通过
为什么使用服务器时间:
- 客户端时间不准:用户的设备时间可能与服务器时间不同,且受到时区、手动更改等因素的影响。通过获取服务器时间,我们确保倒计时的准确性,避免用户通过修改本地时间来操控倒计时。
- 服务器时间一致性:所有用户的倒计时将从一个统一的标准时间开始,避免不同用户看到不同的倒计时结果。
这种方式确保了倒计时的准确性,并且在秒杀活动中能精准地控制按钮的启用和禁用。