在高并发场景下,Node.js 应用往往需要借助网关/负载均衡器来分发流量。相比应用层实现分发逻辑,用 Nginx 处理“百分比分流”更稳定、更高效。本文将介绍一个完整的高并发方案:
- Nginx 百分比分发
- 消息队列处理并发写
- Redis 热点缓存
- 数据库读写分离
概念
- Nginx 百分比分发:通过
upstream和weight配置,控制不同后端服务接收多少流量(例如 70% 到 A,30% 到 B)。 - 消息队列:把并发写请求入队,顺序消费,避免数据冲突。
- Redis 缓存:高频读操作优先读缓存,减少 DB 压力。
- 读写分离:主库写,从库读,应用层或中间件自动分流。
原理
- Nginx 负载均衡:基于权重(
weight),按照比例把请求分配到不同的 Node.js 服务。 - MQ 串行化:写请求进入队列 → 单线程消费 → 顺序写入数据库。
- 缓存:Cache-Aside 模式(读缓存 → DB → 回填)。
- 数据库:主从复制,应用决定走主库还是从库。
实践
1. Nginx 配置:百分比分发
在 /etc/nginx/conf.d/node.conf 新建配置:
upstream node_app {
server 127.0.0.1:4001 weight=70 max_fails=3 fail_timeout=30s; # 服务 A,70% 流量
server 127.0.0.1:4002 weight=30max_fails=3 fail_timeout=30s; # 服务 B,30% 流量
}
server {
listen 3000;
location / {
proxy_pass http://node_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
🔎 说明:
max_fails=3:30 秒内失败 3 次即认为该节点不可用。fail_timeout=30s:在 30 秒内暂停向该节点转发请求。- 到期后 Nginx 会重新尝试,节点恢复后自动重新加入。
👉 启动方式:
nginx -s reload # 重载配置
curl http://127.0.0.1:3000/api/test # 请求将按 70/30 分发到两个 Node 服务
2. MQ:并发写请求串行化
生产者
// mq/producer.js
const amqp = require('amqplib');
async function produce(queue, payload) {
const conn = await amqp.connect('amqp://user:pass@localhost:5672');
const ch = await conn.createChannel();
await ch.assertQueue(queue, { durable: true });
ch.sendToQueue(queue, Buffer.from(JSON.stringify(payload)), { persistent: true });
await ch.close();
await conn.close();
}
module.exports = { produce };
消费者
// mq/consumer.js
const amqp = require('amqplib');
const { updateUser } = require('../db');
async function startConsumer(queue) {
const conn = await amqp.connect('amqp://user:pass@localhost:5672');
const ch = await conn.createChannel();
await ch.assertQueue(queue, { durable: true });
ch.prefetch(1);
ch.consume(queue, async (msg) => {
if (!msg) return;
const payload = JSON.parse(msg.content.toString());
try {
await updateUser(payload.userId, payload.changes);
ch.ack(msg);
} catch (err) {
console.error(`[Consumer Error] ${err.message}`);
ch.nack(msg, false, false);
}
});
}
startConsumer('user-update').catch(console.error);
3. Redis 缓存
// cache.js
const Redis = require('ioredis');
const redis = new Redis();
const { getUserById, updateUserInDB } = require('./db');
async function getUser(userId) {
const key = `user:${userId}`;
let cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const user = await getUserById(userId);
if (!user) return null;
await redis.set(key, JSON.stringify(user), 'EX', 60);
return user;
}
async function updateUser(userId, changes) {
await updateUserInDB(userId, changes);
await redis.del(`user:${userId}`); // 删除缓存,下次请求自动回填
}
4. 数据库读写分离
// db.js
const { Pool } = require('pg');
const writePool = new Pool({ host: 'db-primary', user: 'app', password: 'pwd', database: 'mydb' });
const readPools = [
new Pool({ host: 'db-replica-1', user: 'app', password: 'pwd', database: 'mydb' }),
new Pool({ host: 'db-replica-2', user: 'app', password: 'pwd', database: 'mydb' }),
];
let rr = 0;
function getReadPool() {
rr = (rr + 1) % readPools.length;
return readPools[rr];
}
async function getUserById(id) {
return (await getReadPool().query('SELECT * FROM users WHERE id=$1', [id])).rows[0];
}
async function updateUserInDB(id, changes) {
await writePool.query('UPDATE users SET data=$1 WHERE id=$2', [changes, id]);
}
module.exports = { getUserById, updateUserInDB, updateUser: updateUserInDB };
拓展
- 灰度发布:动态调整
weight实现平滑升级。 - 一致性:对强一致场景,写后读要走主库。
- 缓存雪崩防护:给 TTL 加随机偏移。
- MQ 幂等处理:写请求必须带唯一 ID,避免重复消费。
潜在问题
- Nginx 单点:可部署多实例,前面加云 LB。
- 读写延迟:从库可能存在复制延迟,需在关键场景强制走主库。
- 消息丢失:需开启 RabbitMQ 持久化,并配置死信队列。
- 缓存击穿:热点数据需加互斥锁。
总结
通过 Nginx 百分比分发 + RabbitMQ 串行化写 + Redis 热点缓存 + 数据库读写分离,我们就能在 Node.js 系统中搭建一个支撑“百分级并发”的生产级架构。
Nginx 的引入让分发层更高效、更稳定,也能更好地支持灰度发布和回滚。
最后这种是由于购买大型服务器超贵的解决方案,如果金额足够使用一台大型服务器就足够了。
本文部分内容借助 AI 辅助生成,并由作者整理审核。