Node.js日志并发问题分析
Node.js异步非阻塞I/O模型在高并发场景中易引发竞态条件、死锁、资源泄漏及并发限制等问题。竞态条件因操作顺序混乱导致数据不一致;死锁源于资源循环等待;资源泄漏因未及时关闭文件或连接;并发限制不足会耗尽系统资源。通过串行化操作、统一资源顺序、使用try...finally或限流中间件可有效解决。
Node.js 凭借其异步和非阻塞 I/O 模型,在处理高并发场景时确实得心应手。但硬币的另一面是——这种机制也容易埋下一些“暗坑”。如果不提前了解这些并发陷阱,生产环境随时可能给你一个“惊喜”。下面这张图梳理了常见的几类问题,我们逐一拆解。

1. 竞态条件(Race Conditions)
竞态条件本质上就是“谁先谁后”的混乱。多个异步操作同时盯上同一个共享资源,又没有排队机制,结果就是操作顺序完全不可控,数据不一致简直家常便饭。
一个最直观的例子:
let counter = 0;
function incrementCounter() {
counter++;
}
for (let i = 0; i < 1000; i++) {
incrementCounter();
}
console.log(counter); // 可能不会输出1000
发现问题了?counter++ 这个看似原子性的操作,在异步环境下可能被拆分成“读取-加一-写入”三个步骤,多个操作同时执行就会互相覆盖。解决方案其实不复杂——用 async/await 配合 Promise 把操作串行化,让每一步都等上一步完成后再执行。
let counter = 0;
async function incrementCounter() {
return new Promise((resolve) => {
setTimeout(() => {
counter++;
resolve();
}, 0);
});
}
(async () => {
const promises = [];
for (let i = 0; i < 1000; i++) {
promises.push(incrementCounter());
}
await Promise.all(promises);
console.log(counter); // 输出1000
})();
2. 死锁(Deadlocks)
死锁就像两个人堵在门口,谁都不肯先让一步。在 Node.js 里,典型场景是多个异步操作互相等待对方释放资源,结果谁也走不下去。下面这段代码就是一个“教科书级”的演示:
const fs = require('fs');
fs.readFile('file1.txt', 'utf8', (err, data1) => {
if (err) throw err;
console.log(data1);
fs.readFile('file2.txt', 'utf8', (err, data2) => {
if (err) throw err;
console.log(data2);
});
});
fs.readFile('file2.txt', 'utf8', (err, data2) => {
if (err) throw err;
console.log(data2);
fs.readFile('file1.txt', 'utf8', (err, data1) => {
if (err) throw err;
console.log(data1);
});
});
两个回调函数都在等待对方先完成,结果谁都等不到。破局的关键很简单:统一资源的获取和释放顺序,避免循环等待。比如始终先读 file1 再读 file2,就永远不会出现互相锁死的情况。
3. 资源泄漏(Resource Leaks)
这个坑最常见也最容易忽略——打开的文件句柄、数据库连接,用完就忘了关。积少成多,轻则资源耗尽,重则应用崩溃。看下面这个反面教材:
const fs = require('fs');
function readFile(filePath) {
const file = fs.openSync(filePath, 'r');
const data = fs.readFileSync(file, 'utf8');
console.log(data);
// 忘记关闭文件
}
readFile('file.txt');
文件打开后没有关闭,虽然程序运行完就结束了,但在长时间运行的服务器里,一次调用不关,十次一百次呢?正确的做法是用 try...finally 保障资源释放,或者直接使用 fs.promises 配合异步写法:
const fs = require('fs').promises;
async function readFile(filePath) {
let file;
try {
file = await fs.open(filePath, 'r');
const data = await file.readFile('utf8');
console.log(data);
} finally {
if (file) {
await file.close();
}
}
}
readFile('file.txt');
4. 并发限制(Concurrency Limits)
最后一个问题也是最容易被流量“冲垮”的——没有限制的高并发。Node.js 单线程事件循环虽然高效,但面对海量请求时,如果不加任何限流措施,CPU 和内存很快就会被占满。简单建一个 HTTP 服务不做任何保护,后果可想而知:
const http = require('http');
http.createServer((req, res) => {
// 处理请求
}).listen(3000);
解决思路是引入限流中间件,比如 express-rate-limit,按 IP 或全局设置访问频率上限:
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100 // 每个IP每15分钟最多100个请求
});
app.use(limiter);
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000);
理解这四种并发问题的成因和对应的解决手段,Node.js 应用的稳定性和性能就有了基本保障。调试时如果遇到诡异的数据不一致、进程卡死或内存暴涨,可以优先从这几个方向排查——大概率能帮你省下不少时间。
