高并发系统设计:我在北京出租屋里踩过的坑和学到的招
上周五晚上十一点,窗外北京五环外的夜色早已沉寂,我盯着屏幕上不断飙升的 CPU 曲线,心里默默问候了产品经理全家。事情是这样的:一个原本只是内部用的小工具 API,因为被某个“聪明”的前端同学不小心暴露在公网,突然之间每秒请求量从个位数飙到了 5000+。而我这个远程办公、孤军奋战的独立开发者,连个能一起背锅的人都没有。
说来有点自嘲——作为一个常年在家 coding 的人,我本以为高并发这种词只存在于大厂 PPT 和面试题里。结果现实狠狠打脸:只要你的服务能被访问,就可能被压垮。那晚,我一边重启崩溃的 Node.js 进程,一边下定决心:得认真搞懂高并发系统设计了。
起因:一个被“误伤”的小接口
事情还得从头说起。我最近接了个外包项目,用 Express + MongoDB 搭了个简单的用户行为日志收集服务。前端每触发一个按钮点击,就往我的 /log 接口发个 POST 请求。一开始日活就几百人,完全没压力。
但上周产品上线了一个新功能,把日志上报逻辑错误地放在了首屏加载里——而且是同步阻塞的那种。更糟的是,他们用了轮询,每 2 秒上报一次。于是,当活动页上线后,瞬间流量爆炸。
“兄弟,你这接口怎么挂了?我们页面白屏了!”
——前端同事凌晨 1 点微信轰炸
我当时就想:这不就是典型的高并发场景吗?虽然规模不大(对比双11那种),但对我这个单机部署、无缓存、无限流的小服务来说,已经算“洪峰”了。
别再只背面试题了,实战才是硬道理
说实话,以前刷 LeetCode、看《高性能 MySQL》的时候,总觉得“高并发”离我很远。毕竟我是个自由开发者,接的多是中小型项目。但这次事故让我意识到:哪怕你写的是 JavaScript,也逃不开系统设计的基本功。
很多教程一上来就讲“CAP 理论”、“分布式锁”、“消息队列削峰”,听着高大上,但对像我这样一个人干活的人来说,太重了。我们需要的是可落地、低成本、见效快的方案。
所以,我决定从零开始,用实战的方式重构这个日志服务。目标很明确:扛住 5000 QPS,延迟 < 100ms,资源占用可控。
第一步:别让请求直接砸到数据库
最开始的架构简直惨不忍睹:
Client → Express → MongoDB
所有请求直插数据库,连个缓冲都没有。MongoDB 的连接池很快耗尽,CPU 打满,Node.js 主线程阻塞,雪崩效应直接拉胯。
解法:引入内存队列 + 异步写入
我立刻加了一层内存队列(用 bull 这个 Redis-based 的任务队列库),把写操作异步化:
// routes/log.js
const Queue = require('bull');
const logQueue = new Queue('log queue', 'redis://127.0.0.1:6379');
app.post('/log', async (req, res) => {
// 快速响应,不等 DB
await logQueue.add({ payload: req.body });
res.status(200).json({ ok: true });
});
然后后台起一个 worker,批量消费:
// workers/logWorker.js
logQueue.process(async (job) => {
const { payload } = job.data;
// 批量插入,减少 DB 压力
await LogModel.insertMany([payload]);
});
效果立竿见影:接口响应时间从 300ms 降到 10ms 以内,MongoDB 的负载直线下降。
经验教训:高并发下,快速释放请求线程比“做完所有事”更重要。能异步的,绝不同步。
第二步:缓存不是万能的,但没有缓存是万万不能的
虽然日志服务本身不需要读缓存,但我在另一个项目(用户配置中心)里深刻体会到缓存的价值。
有一次,一个高频接口每秒被调几千次,查的是同一个用户配置。数据库天天报警。后来我加了 Redis 缓存,TTL 60 秒,QPS 直接降了 99%。
// 伪代码
async function getUserConfig(userId) {
const cacheKey = `user:config:${userId}`;
let config = await redis.get(cacheKey);
if (!config) {
config = await db.getUserConfig(userId);
await redis.setex(cacheKey, 60, JSON.stringify(config));
}
return JSON.parse(config);
}
关键点:缓存策略要根据数据更新频率来定。如果是实时性要求高的,可以用 Cache-Aside 模式;如果是允许短暂不一致的,TTL + 异步刷新更稳。
第三步:限流——保护自己,也保护下游
回到日志服务,虽然用了队列,但如果流量持续暴涨,内存还是会爆。于是我又加上了限流。
Node.js 生态里,express-rate-limit 是个简单好用的选择:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 分钟
max: 100, // 每 IP 最多 100 次
message: 'Too many requests from this IP',
});
app.use('/log', limiter);
但注意:IP 限流在 NAT 或 CDN 场景下会失效(比如所有用户都走 Cloudflare)。这时候可以结合 Token Bucket 或滑动窗口算法,用 Redis 实现更精准的限流。
我后来改用 rate-limiter-flexible + Redis,支持按用户 ID 限流,效果更好。
数据库设计:别让慢查询拖垮整个系统
高并发下,一个慢查询就能让整个 DB 卡死。我在重构时做了几件事:
- 加索引:所有查询字段必须有索引,尤其是时间戳、用户 ID。
- 分表分库?先别急:日志数据我按天分 collection(MongoDB 的集合),避免单表过大。
- 写优化:用
insertMany批量写,而不是一条条 insert。 - 读写分离?没必要:小项目搞主从太重,不如先优化查询。
下面是我优化前后的性能对比:
| 优化项 | QPS (单机) | 平均延迟 | CPU 使用率 |
|---|---|---|---|
| 原始版本 | ~80 | 320ms | 95%+ |
| 加队列 + 异步写 | ~3000 | 12ms | 40% |
| 再加限流 + 缓存 | ~5000+ | 8ms | 35% |
关于 JavaScript 的“高并发”能力
很多人说:“JS 是单线程,搞不了高并发”。这话一半对一半错。
- 对:Node.js 主线程确实是单线程,CPU 密集型任务会阻塞。
- 错:I/O 密集型场景(比如 API 网关、日志收集)正是 Node.js 的强项,靠事件循环 + 非阻塞 I/O,轻松 handle 上万并发连接。
关键在于别在主线程干重活。像加密、图像处理这种,要么扔给 Worker Thread,要么用外部服务(比如 Rust 写的微服务,嘿嘿,最近正研究这个)。
说到 Rust——上周我试着用 Actix 写了个日志接收器,同样 5000 QPS,内存占用只有 Node.js 的 1/3,延迟更低。虽然生态不如 JS 成熟,但性能确实香。不过对于大多数 Web 项目,Node.js + 合理架构完全够用。
面试题背后的真相
现在回想那些“高并发系统设计”面试题,比如:
“如果让你设计一个秒杀系统,你会怎么做?”
以前我只会背答案:缓存预热、库存扣减用 Redis Lua、消息队列削峰、限流熔断……
但真正动手后才发现:理论是骨架,实践才是血肉。比如:
- Redis Lua 脚本写错了,会导致库存超卖;
- 消息队列积压了,怎么监控和告警?
- 限流阈值设多少?怎么动态调整?
这些细节,只有在线上跑过才知道。
我的实战 Checklist
经过这次折腾,我总结了一套“小团队高并发应急包”:
- ✅ 快速响应:接口先返回,异步处理
- ✅ 队列削峰:用 Redis 或 Kafka 缓冲写压力
- ✅ 合理限流:按 IP / 用户 / Token 限制
- ✅ 缓存兜底:热点数据缓存,减少 DB 压力
- ✅ 监控告警:至少要有 CPU、内存、QPS、错误率监控
- ✅ 压测验证:上线前用
artillery或k6压一下
# 用 artillery 做简单压测
artillery quick --count 1000 -n 50 http://localhost:3000/log
最后:孤独开发者的自救指南
作为在北京租房、每天通勤一小时(其实是去咖啡馆写代码)、偶尔参加技术分享会的独立开发者,我深知一个人扛系统的压力。没有 SRE,没有 DBA,出问题只能自己修。
但这也逼着我成长。每一次线上事故,都是一次免费的“高并发实战课”。
如果你也在做类似的小项目,别怕。高并发不是大厂专利,而是每个认真对待线上服务的开发者的必修课。从 10 QPS 到 10000 QPS,中间没有魔法,只有一个个具体的优化点。
下次再遇到流量洪峰,我希望你能笑着说:“来吧,我队列都 ready 了。”
本文完。写于北京回龙观某出租屋,窗外正在下雨。刚跑完压测,服务稳如老狗。
下周打算用 Rust 重写核心模块,欢迎交流。
—— 一个不想再半夜被微信叫醒的独立开发者

评论 0