*(图:一个Node.js服务器监控面板,显示并发请求从3000飙升到8000时CPU和内存的变化曲线)*
先看第一个坑:数据库连接池配不对,请求一多就挂
项目初期我用的直接是单连接方式,每次请求都创建新连接。这在并发低时没问题,一旦超过200并发,数据库连接池耗尽,后续请求全部排队超时。
为什么这么写? 因为官方文档把连接池配置写得太隐蔽了,我直接跳过了。
“javascript
// 错误写法:每次请求都创建新连接
const mysql = require('mysql2');
app.get('/users', async (req, res) => {
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'password',
database: 'test'
});
const [rows] = await connection.promise().query('SELECT * FROM users');
res.json(rows);
});
`
这个设计真的反人类——每个请求都创建TCP连接,开销巨大。用压力测试工具wrk测一下,200并发下每秒只能处理120个请求,而且连接数飙升到1500+。
正确做法:使用连接池,并合理配置池参数。
`javascript
// 正确写法:连接池复用
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'test',
waitForConnections: true, // 池满时请求等待
connectionLimit: 30, // 最大连接数(根据CPU核数*2+磁盘数估算)
queueLimit: 0, // 不限制等待队列
enableKeepAlive: true, // 保持长连接
keepAliveInitialDelay: 10
<
p>000 // 10秒后开始保活
});
// 中间件注入连接池
app.use((req, res, next) => {
req.db = pool;
next();
});
app.get('/users', async (req, res) => {
const [rows] = await req.db.query('SELECT * FROM users');
res.json(rows);
});
`
改完后,同样200并发,每秒处理量从120飙到850,连接数稳定在30个左右。连接池就是Node.js的高并发命根子。
另一个坑:中间件顺序就是生命线
有次我写了一个认证中间件,结果发现所有接口都返回401,排查了3小时才发现是中间件注册顺序错了。
`javascript`
// 错误顺序
app.use(express.json()); // 解析JSON请求体
app.use('/api', authMiddleware); // 认证中间件
app.get('/api/users', userHandler); // 实际路由
app.use(errorHandler); // 错误处理中间件
看起来没问题对吧?但坑在:authMiddleware放在了express.json()后面,但如果某个路由没有经过authMiddleware,它就无法访问req.body。更严重的是,如果errorHandler放在路由前面,错误就永远不会被捕获。
正确的中间件顺序:全局错误处理必须在最后。
`javascript
const express = require('express');
const app = express();
// 1. 解析中间件(先注册)
app.use(express.json({ limit: '10kb' })); // 限制请求体大小防DDOS
app.use(express.urlencoded({ extended: true }));
// 2. 安全中间件
const helmet = require('helmet');
app.use(helmet());
// 3. 日志中间件
const morgan = require('morgan');
app.use(morgan('combined'));
// 4. 自定义中间件(认证、限流等)
const rateLimit = require('express-rate-limit');
const
limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 每个IP最多100个请求
message: '请求过于频繁,请稍后再试'
});
app.use('/api', limiter);
// 5. 路由中间件
app.use('/api/users', userRouter);
app.use('/api/posts', postRouter);
// 6. 最后:全局错误处理中间件(必须最后注册)
app.use((err, req, res, next) => {
console.error([${new Date().toISOString()}] Error:, err.stack);`
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production' ? '服务器内部错误' : err.message
});
});
中间件顺序就像流水线,每个环节都有职责,搞乱顺序就崩。还有个技巧:用express-async-errors包自动捕获异步错误,省得每个路由都写try-catch。
*(图:Express中间件执行顺序的流程图,从请求进入一路向下再到错误处理)*
第三个坑:错误处理太随意,生产环境直接暴露内部信息
很多人写接口时错误处理就一个console.log + 500,结果线上报错时用户看到“Cannot read property 'id' of undefined”,直接暴露了数据库结构。更可怕的是,没有统一错误格式,前端根本不知道怎么处理。
错误处理必须做到三件事:统一格式、分层处理、生产环境屏蔽细节。
`javascript
// 自定义错误类
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true; // 标记为可预见的操作错误
Error.captureStackTrace(this, this.constructor);
}
}
// 404错误处理
app.all('*', (req, res, next) => {
next(new AppError(路由 ${req.originalUrl} 不存在, 404));
});
// 全局错误处理中间件
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
// 生产环境:只返回错误信息和状态码
if (process.env.NODE_ENV === 'production') {
// 区分操作错误和编程错误
if (err.isOperational) {
res.status(err.statusCode).json({
status: 'error',
message: err.message
});
} else {
// 编程错误记录日志但返回通用信息
console.error('UNEXPECTED ERROR:', err);
res.status(500).json({
status: 'error',
message: '服务器内部错误'
});
}
} else {
// 开发环境:返回完整错误信息
res.status(err.statusCode).json({
status: 'error',
message: err.message,
stack: err.stack,
error: err
});
}
});
`
这还没完,实际业务中得用统一的响应格式,我推荐这样:
`javascript
// 统一响应格式
class ApiResponse {
static success(data, message = '成功') {
return {
code: 200,
message,
data
};
}
static fail(code = 400, message = '请求失败', data = null) {
return {
code,
message,
data
};
}
}
// 路由中使用
app.get('/api/users', async (req, res, next) => {
try {
const users = await User.find();
res.json(ApiResponse.success(users));
} catch (err) {
next(new AppError('获取用户列表失败', 500));
}
});
`
这样前端不管成功失败,都能统一解析{ code, message, data },不用再写一堆if-else判断。
还有个技巧:用Joi做参数校验,别自己写正则
我见过太多人自己写正则校验邮箱、手机号,结果漏掉各种边界情况。推荐用Joi库,声明式校验,代码干净:
`javascript
const Joi = require('joi');
const userSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')).required(),
age: Joi.number().integer().min(0).max(150).optional()
});
// 中间件封装
const validate = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.body, { abortEarly: false });
if (error) {
const messages = error.details.map(detail => detail.message);
return res.status(400).json(ApiResponse.fail(400, '参数校验失败', messages));
}
next();
};
};
app.post('/api/users', validate(userSchema), async (req, res) => {
// 经过校验,req.body数据安全可靠
const user = await User.create(req.body);
res.status(201).json(ApiResponse.success(user, '用户创建成功'));
});
`
Joi的错误信息默认是英文,可以用messages选项自定义中文提示:“用户名长度必须在3-30字符之间”,比前端自己拼错误提示好多了。
*(图:压力测试结果对比图,左边无优化版本每秒处理300请求,右边经过连接池、中间件优化后每秒处理10000请求)*
总结一下,你可以立刻用的三个点
的createPool,参数设置connectionLimit为CPU核数*2+磁盘数,waitForConnections设为true。注册4个参数。类,生产环境屏蔽栈信息,用ApiResponse`统一响应格式,配合Joi做参数校验。这套组合拳打下来,我的接口从上线三天就宕机,到现在稳定运行半年,日均处理20万请求无压力。踩过的坑都写出来了,你照着改就行。
本文由AI辅助创作,仅供参考。