高并发系统设计:从零开始构建你的第一个抗压后端
大家好,我是老张,一名985毕业的全栈工程师,平时在掘金写了不少入门教程。今天这篇《高并发系统设计:从零开始构建你的第一个抗压后端》,其实源于我带实习生时的一个真实场景——很多同学一听到“高并发”就发怵,以为这是只有大厂架构师才碰的东西。但其实,高并发系统设计的核心思想并不神秘,只要掌握基本原理,再配合实践,你也能写出扛得住流量的后端服务。
我当初学的时候,也是一头雾水。看到“QPS 10万+”、“分布式缓存”、“限流熔断”这些词,感觉像是天书。但后来发现,高并发的本质,就是“让系统在大量用户同时访问时依然能正常工作”。今天这篇教程,就带你用最简单的语言、最真实的代码,一步步走进高并发的世界。
一、什么是高并发?它到底用来解决什么问题?
想象一下:你开发了一个抢购小程序,上线第一天只有10个人用,没问题;但第二天突然上了热搜,10万人同时点“立即购买”——如果系统没做任何优化,大概率会直接崩溃:页面打不开、订单重复、数据库卡死……
这就是典型的高并发场景。
高并发(High Concurrency) 指的是系统在同一时间段内处理大量请求的能力。
常见的高并发场景包括:
- 秒杀/抢购活动
- 热门文章/视频的点赞评论
- 支付系统高峰期
- 社交平台热门话题
我们的目标不是一开始就要支撑百万QPS(每秒查询数),而是学会一套可扩展的设计思路,让你的小项目也能平稳应对流量高峰。
二、环境准备:搭建一个能跑起来的实验场
为了动手实践,我们需要一个最小化的开发环境。别担心,全是免费工具!
所需工具清单
| 工具 | 用途 | 安装方式 |
|---|---|---|
| Node.js (v18+) | 运行JavaScript后端 | 官网下载或 nvm install 18 |
| Redis | 缓存与限流 | Docker: docker run -p 6379:6379 redis |
| MySQL | 数据库 | Docker: docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql |
| Postman | 测试API | 官网下载 |
💡 新手提示:如果你还没用过Docker,可以直接安装Redis和MySQL本地版,但Docker更干净、隔离性更好,强烈推荐学起来!
初始化项目
mkdir high-concurrency-demo
cd high-concurrency-demo
npm init -y
npm install express redis mysql2 dotenv
创建 .env 文件:
DB_HOST=localhost
DB_USER=root
DB_PASS=123456
REDIS_URL=redis://localhost:6379
现在,你的实验环境就 ready 了!
三、核心概念:高并发系统的四大支柱
高并发不是靠“堆服务器”就能解决的。真正的关键是合理利用资源 + 避免瓶颈。下面四个概念,是所有高并发系统的基础。
1. 缓存(Cache)——把热点数据放快的地方
数据库很慢,磁盘I/O是毫秒级;而内存(如Redis)是微秒级。把经常读、很少改的数据缓存起来,能极大减轻数据库压力。
我当初第一次用缓存,是在一个商品详情页项目里。每次刷新都查数据库,结果100人同时访问,数据库CPU直接飙到100%。加上Redis缓存后,QPS从50提升到5000+!
代码示例:用Redis缓存商品信息
// utils/cache.js
const redis = require('redis');
const client = redis.createClient({ url: process.env.REDIS_URL });
client.connect();
async function getFromCache(key) {
const data = await client.get(key);
return data ? JSON.parse(data) : null;
}
async function setToCache(key, value, ttl = 60) { // 默认60秒过期
await client.setEx(key, ttl, JSON.stringify(value));
}
module.exports = { getFromCache, setToCache };
// routes/product.js
const express = require('express');
const router = express.Router();
const db = require('../utils/db'); // 假设已封装MySQL连接
const { getFromCache, setToCache } = require('../utils/cache');
router.get('/:id', async (req, res) => {
const id = req.params.id;
const cacheKey = `product:${id}`;
// 先查缓存
let product = await getFromCache(cacheKey);
if (!product) {
// 缓存未命中,查数据库
product = await db.query('SELECT * FROM products WHERE id = ?', [id]);
if (product.length > 0) {
// 写入缓存,下次直接返回
await setToCache(cacheKey, product[0]);
}
}
res.json(product[0] || { error: 'Not found' });
});
✅ 避坑指南:缓存一定要设置过期时间!否则数据更新后,用户会一直看到旧内容(这叫“缓存雪崩”的前兆)。
2. 异步处理(Async)——别让用户干等
有些操作很耗时(比如发邮件、生成报表),如果同步执行,用户会一直卡在页面上。把非关键路径的操作放到后台异步处理,能显著提升响应速度。
代码示例:用队列模拟异步下单
// utils/queue.js
const EventEmitter = require('events');
const queueEmitter = new EventEmitter();
// 模拟后台处理
queueEmitter.on('order_created', async (order) => {
console.log(`正在处理订单 ${order.id}...`);
await sendEmail(order.user_email); // 耗时操作
console.log(`订单 ${order.id} 处理完成`);
});
function addToQueue(event, data) {
setImmediate(() => queueEmitter.emit(event, data)); // 模拟异步
}
module.exports = { addToQueue };
// routes/order.js
router.post('/', async (req, res) => {
const { user_id, product_id } = req.body;
// 1. 先快速入库(保证数据不丢)
const orderId = await db.insert('orders', { user_id, product_id, status: 'pending' });
// 2. 立即返回成功,不等发邮件
res.status(201).json({ id: orderId, message: 'Order received' });
// 3. 异步处理后续逻辑
addToQueue('order_created', { id: orderId, user_email: 'user@example.com' });
});
✅ 开发心得:在真实项目中,你会用 RabbitMQ、Kafka 或 Bull(Node.js)这样的专业队列。但原理一样:快路径返回,慢路径异步。
3. 限流(Rate Limiting)——保护系统不被冲垮
即使有缓存和异步,如果瞬间涌入10万请求,系统还是会挂。限流就像高速公路的收费站,控制进入系统的流量速度。
常用算法:
- 计数器:简单粗暴,每秒最多100次
- 令牌桶:允许突发流量,但平均速率可控
- 漏桶:匀速处理,平滑输出
代码示例:基于Redis的简单计数器限流
// middleware/rateLimit.js
const { getFromCache, setToCache } = require('../utils/cache');
async function rateLimit(req, res, next) {
const ip = req.ip || req.connection.remoteAddress;
const key = `rate_limit:${ip}`;
let count = await getFromCache(key);
if (count === null) {
count = 0;
await setToCache(key, 0, 60); // 60秒窗口
}
if (count >= 10) { // 每分钟最多10次
return res.status(429).json({ error: 'Too many requests' });
}
await setToCache(key, count + 1, 60);
next();
}
module.exports = rateLimit;
在路由中使用:
const rateLimit = require('../middleware/rateLimit');
router.get('/api/data', rateLimit, (req, res) => {
// 你的业务逻辑
});
⚠️ 注意:这个实现是简化的。生产环境建议用
express-rate-limit这样的成熟库。
4. 数据库优化 —— 别让SQL成为瓶颈
高并发下,数据库是最容易出问题的环节。三个关键点:
- 索引优化:WHERE、JOIN 字段必须加索引
- 读写分离:主库写,从库读
- 分库分表:当单表超过千万行,考虑拆分
新手必看:一个慢查询的优化过程
假设你有这样一条SQL:
SELECT * FROM orders WHERE user_id = 123 ORDER BY created_at DESC;
如果没有索引,每次都要全表扫描。加上复合索引:
CREATE INDEX idx_user_created ON orders(user_id, created_at);
查询速度从 200ms 降到 2ms!
💡 开发心得:用
EXPLAIN分析SQL执行计划,是每个后端工程师的必备技能。
四、实战项目:构建一个简易秒杀系统
现在,我们把前面的知识串起来,做一个能抗住一定并发的秒杀接口。
需求
- 商品库存100件
- 用户点击“抢购”按钮
- 成功返回“抢购成功”,失败返回“已售罄”
- 防止超卖(不能卖出101件)
步骤1:数据库设计
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(100),
stock INT NOT NULL
);
INSERT INTO products VALUES (1, '限量球鞋', 100);
步骤2:核心逻辑 —— 用Redis原子操作防超卖
// routes/seckill.js
const { getFromCache, setToCache } = require('../utils/cache');
const db = require('../utils/db');
router.post('/seckill/:productId', async (req, res) => {
const productId = req.params.productId;
const userId = req.body.user_id; // 假设已登录
// 1. 先检查是否已抢过(防重复)
const hasBought = await getFromCache(`seckill:user:${userId}:product:${productId}`);
if (hasBought) {
return res.json({ error: '每人限购一件' });
}
// 2. 尝试扣减库存(Redis原子操作)
const stockKey = `stock:${productId}`;
const currentStock = await getFromCache(stockKey);
if (currentStock === null) {
// 首次加载库存到Redis
const dbProduct = await db.query('SELECT stock FROM products WHERE id = ?', [productId]);
if (dbProduct.length === 0 || dbProduct[0].stock <= 0) {
return res.json({ error: '已售罄' });
}
await setToCache(stockKey, dbProduct[0].stock, 3600); // 缓存1小时
return this.seckill(req, res); // 递归重试
}
if (parseInt(currentStock) <= 0) {
return res.json({ error: '已售罄' });
}
// 关键:用DECR原子减1
const newStock = await redisClient.decr(stockKey);
if (newStock < 0) {
// 超卖了,回滚
await redisClient.incr(stockKey);
return res.json({ error: '已售罄' });
}
// 3. 记录用户已购买
await setToCache(`seckill:user:${userId}:product:${productId}`, '1', 3600);
// 4. 异步写入订单(这里简化)
console.log(`用户 ${userId} 抢购成功!剩余库存: ${newStock}`);
res.json({ message: '抢购成功!', stock_left: newStock });
});
🔑 为什么用Redis做库存?
因为Redis的DECR是原子操作,多个请求同时执行也不会出错。而数据库的UPDATE stock = stock - 1在高并发下可能因事务隔离级别导致超卖。
五、新手常见问题解答
Q1:我的本地测试只有几个人,怎么模拟高并发?
用 artillery 或 wrk 压测工具:
npm install -g artillery
创建 load-test.yml:
config:
target: 'http://localhost:3000'
phases:
- duration: 30
arrivalRate: 100 # 每秒100个请求
scenarios:
- flow:
- post:
url: "/seckill/1"
json:
user_id: "{{ $randomInt(1, 10000) }}"
运行:artillery run load-test.yml
Q2:缓存和数据库如何保持一致?
这是经典难题!简单策略:
- Cache-Aside Pattern:先更新数据库,再删除缓存(下次读时重建)
- 设置合理过期时间(如30秒),容忍短暂不一致
Q3:限流后用户看到429错误,体验很差怎么办?
可以:
- 返回友好提示:“系统繁忙,请稍后再试”
- 加入排队机制(如“您前面还有123人”)
- 用验证码分流机器人流量
六、下一步学习建议
恭喜你!已经掌握了高并发系统的核心思想。接下来,你可以:
- 深入缓存:学习Redis持久化、集群模式
- 消息队列实战:用RabbitMQ/Kafka重构异步逻辑
- 分布式锁:解决跨服务的资源竞争问题
- 监控与告警:用Prometheus + Grafana观察系统指标
最后送大家一句话:高并发不是一蹴而就的,而是一步步演进出来的。先做出能用的系统,再根据瓶颈逐个优化。不要一上来就想设计淘宝双11架构——那只会让你陷入焦虑。
希望这篇教程能帮你迈出高并发学习的第一步。如果觉得有用,欢迎在掘金关注我,我会持续更新更多实战教程!
作者:老张(985全栈工程师 | 掘金技术博主)
声明:本文所有代码均可在 GitHub 仓库获取(搜索 “high-concurrency-demo-nodejs”)

评论 0