高并发系统设计:从零开始构建你的第一个抗压后端

杰出之战士
2025-12-26 20:03
阅读 606

大家好,我是老张,一名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成为瓶颈

高并发下,数据库是最容易出问题的环节。三个关键点:

  1. 索引优化:WHERE、JOIN 字段必须加索引
  2. 读写分离:主库写,从库读
  3. 分库分表:当单表超过千万行,考虑拆分

新手必看:一个慢查询的优化过程

假设你有这样一条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:我的本地测试只有几个人,怎么模拟高并发?

artillerywrk 压测工具:

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人”)
  • 用验证码分流机器人流量

六、下一步学习建议

恭喜你!已经掌握了高并发系统的核心思想。接下来,你可以:

  1. 深入缓存:学习Redis持久化、集群模式
  2. 消息队列实战:用RabbitMQ/Kafka重构异步逻辑
  3. 分布式锁:解决跨服务的资源竞争问题
  4. 监控与告警:用Prometheus + Grafana观察系统指标

最后送大家一句话:高并发不是一蹴而就的,而是一步步演进出来的。先做出能用的系统,再根据瓶颈逐个优化。不要一上来就想设计淘宝双11架构——那只会让你陷入焦虑。

希望这篇教程能帮你迈出高并发学习的第一步。如果觉得有用,欢迎在掘金关注我,我会持续更新更多实战教程!


作者:老张(985全栈工程师 | 掘金技术博主)
声明:本文所有代码均可在 GitHub 仓库获取(搜索 “high-concurrency-demo-nodejs”)

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝