AIHub

后端从零开始:一次请求的一生、并发、缓存与 Docker 打包

入门18 分钟读完2026-08-06#后端#入门教程#数据库#并发#Docker
后端从零开始:一次请求的一生、并发、缓存与 Docker 打包

很多人学前端出身,或做产品、运营想补技术,打开后端教程的第一反应是被名词淹没:TCP、索引、事务、锁、消息队列、容器……其实后端的核心概念就那么几个,而且每一个都有非常生活化的直觉。这篇文章不堆术语,按"一次请求从浏览器到数据库再回来"的主线,把后端最重要的几块知识串起来,每个概念配一张图。

一、后端到底在干什么:餐厅的后厨

把一家餐厅想成一个网站:

  • 前端是前厅:装修、菜单、服务员——用户看得到、摸得着的一切。
  • 后端是后厨:接单、配菜、炒菜、出餐——用户看不到,但决定体验的核心。
  • 数据库是仓库:食材(数据)的存储、盘点、进出库记录。
  • API 是传菜口:前厅和后厨唯一的沟通通道,菜单(接口文档)规定了"能点什么、怎么点"。

用户在页面上点"提交订单",就像服务员把单子递进传菜口:后厨验单(参数校验)、查库存(查数据库)、炒菜(业务逻辑)、出餐(返回响应)。后端的全部工作,概括起来就是四件事:接收请求、处理逻辑、读写数据、返回结果。剩下的所有概念,都是为了让这四件事更快、更稳、更安全。

二、一次请求的一生

你在浏览器输入网址按下回车,到页面出现,中间发生了什么?

一次请求从浏览器到数据库的完整链路

  1. DNS 解析:域名是给人看的,机器只认 IP。浏览器先问 DNS"example.com 在哪",拿到 IP——像查通讯录。
  2. TCP 握手:浏览器和服务器建立连接,三次握手确认"你能听到我吗、我能听到你"。HTTPS 还要再加一轮 TLS 握手交换密钥。
  3. HTTP 请求:浏览器发出一份格式化的文本:方法(GET/POST)、路径(/orders)、头部(身份凭证、内容类型)、可选的请求体。
  4. 服务器处理:这是后端的主场——路由匹配到对应的处理函数,做参数校验、鉴权、业务逻辑,期间可能要查好几次数据库。
  5. 数据库读写:服务器把 SQL 发给数据库,取回结果或写入数据。
  6. HTTP 响应:服务器把结果(HTML/JSON)按 HTTP 格式包好,沿原路返回。浏览器拿到后渲染成你看到的页面。

前后端的分界就在第 3 和第 6 步之间:前端负责"发出正确的请求、展示返回的数据",后端负责中间的一切。理解这个链路后,调试问题就有了地图:页面没数据,是请求没发对(前端)、处理报错了(后端)、还是数据本身没有(数据库)?分段排查,而不是从头慌到尾。

排查时最有用的线索是 HTTP 状态码,它是服务器对每次请求的"一句话结论":200 成功;301/302 跳转,让浏览器换个地址;400 请求本身有问题(参数错了);401/403 没登录或没权限;404 资源不存在;500 服务器内部出错——看到 500 直接去翻后端日志,看到 4xx 先检查请求。新手和后端沟通时,把"页面不行了"换成"这个接口返回 500",效率立刻高一个档次。

三、数据库:表、索引与事务

表就是 Excel,但有规矩

关系型数据库的表和 Excel 几乎同构:列是字段(姓名、价格),行是记录。区别在于数据库对每一列有类型约束(价格必须是数字)和约束条件(邮箱不能重复),这正是它比 Excel 可靠的原因。SQL 只需要先记住四个动作:SELECT 查、INSERT 增、UPDATE 改、DELETE 删,加上 WHERE 条件过滤,就能覆盖八成日常操作。

索引:书的目录

索引就像书的目录

没有索引时,查"手机尾号 8888 的用户"要把全表逐行翻一遍(全表扫描),一百万行就翻一百万次。索引像书的目录:数据库提前按某个字段排好序建好指引,查询直达目标,从几十万次比较变成几次查找。

代价也直觉可感:目录本身占页数(索引占存储),而且每加一章就要更新目录(写入变慢)。所以索引不是越多越好——给经常出现在 WHERE 里的列建,几乎不用于查询的列不建。

事务:要么全成,要么全不成

转账是教科书例子:A 扣 100、B 加 100,这两步必须同时成功。如果扣完钱系统宕机,B 没收到,钱就凭空消失了。事务把多步操作打包成一个原子单位:任何一步失败,全部回滚到开始前。记住"转账例子"这个画面,你就理解了事务存在的意义。

顺手提一个新手高频坑:N+1 查询

展示"订单列表,每单带用户名",直觉写法是先查出 100 个订单,再循环里逐单查用户——1 次查询变成 101 次,列表页直接卡死。解法是JOIN 一次查回,或者用 IN 批量查询。以后看到"循环里查数据库"这六个字,就要本能地警觉。

四、并发问题:为什么最后一件商品会被卖两次(重点)

超卖是怎么发生的

假设库存只剩 1 件,两个用户同时点"购买"。很多新手写的代码逻辑是:

1. 查库存:SELECT stock FROM products WHERE id = 1
2. 如果 stock > 0,继续
3. 扣库存:UPDATE products SET stock = stock - 1

单用户时毫无问题。但并发的残酷在于时间线是交错的:

时刻1  用户A 查库存 → 1
时刻2  用户B 查库存 → 1(A 还没扣!)
时刻3  用户A 扣库存 → 0,下单成功
时刻4  用户B 扣库存 → -1,下单也成功

两个请求同时抢到同一件库存

两个请求都通过了"库存 > 0"的检查,各下一单,库存变成 -1——超卖。这类 bug 叫竞态条件(race condition):结果取决于多个操作的执行先后顺序,而并发时顺序不受你控制。它本地测试永远测不出来,因为单机很难精确复现"同时",上线后流量一大就爆雷。

解法一:悲观锁

"先锁住,谁都别动":查询时用 SELECT ... FOR UPDATE 把这一行锁住,其他事务排队等你办完。简单可靠,代价是并发度下降——大家都在排队。

解法二:乐观锁(更常用)

不加锁,而是给数据加个版本号,更新时检查"我读的版本还是不是当前版本":

UPDATE products SET stock = stock - 1, version = version + 1
WHERE id = 1 AND stock > 0;

注意这个写法把检查和扣减合并成了一条原子 SQL——数据库保证单条 UPDATE 不会被打断。如果库存已被别人扣成 0,WHERE stock > 0 不成立,影响行数为 0,应用层就知道"没抢到",返回售罄。没有锁的开销,高并发下性能更好,这是库存类场景的标准答案。

幂等性:同一件事,做几次结果都一样

并发之外还有一个孪生问题:用户网络卡了连点三次"提交订单",或者客户端超时自动重试——同一个请求可能到达服务器多次。如果每到一次就下一单,用户会被扣三次钱。

幂等设计的目标:无论请求来几次,效果等同于来一次。常见做法:

  • 客户端每次下单生成一个唯一单号,数据库给单号加唯一约束——重复提交直接插入失败,安全拦截
  • 支付回调等场景用状态机:只有"待支付"才能流转到"已支付",重复回调来了发现状态已变,直接忽略

什么时候不用过度设计

反过来也要说:如果你的应用是内部工具、日活几十人,悲观乐观锁都可以先不管——先把逻辑写对,加唯一约束兜底,并发方案等真有量了再上。架构的复杂度应该配得上业务的规模,教程里的方案是工具箱,不是必做清单。

还有一个常见的进阶问题:多机部署时,单靠数据库行锁管不住"多台服务器同时处理"的场景,需要分布式锁(如 Redis 实现)或把同一商品的请求路由到同一队列串行处理。但请记住顺序:单机方案没跑明白之前,不要碰分布式方案——分布式解决的是规模问题,顺手也会带来十倍的新问题。

五、缓存与队列:给数据库请两个帮手

缓存挡在数据库前,队列把慢任务异步化

缓存(如 Redis):数据库查询再快也是磁盘/网络操作,热点数据(首页商品列表、验证码)放进内存型缓存,响应从几十毫秒降到亚毫秒。直觉理解:数据库是城郊大仓库,缓存是店门口的小货架——高频商品放门口,不用次次跑仓库。注意缓存是"挡在数据库前面削峰"的,不是数据源,数据仍以数据库为准。

用缓存还要知道两个经典事故:缓存穿透——有人故意狂查不存在的 id,缓存永远落空,请求全部打在数据库上(对策:空结果也缓存一小段时间);缓存雪崩——大批缓存同一时间过期,流量瞬间全部涌向数据库(对策:过期时间加随机抖动,热点数据提前续期)。记住这两个词,面试和实战都会反复遇到。

消息队列:下单成功后要发短信、更新统计、通知仓库——这些和"告诉用户下单成功"没有先后依赖。同步做完再返回,用户要等很久。队列的做法:主流程只往队列里丢一条"发短信"的任务就立刻返回,后台 worker 慢慢消费执行。把慢操作异步化,接口响应速度和稳定性同时受益。队列还有一个隐藏价值是削峰填谷:秒杀瞬间一万个下单请求涌进来,先全部进队列排队,后端按自己的能力匀速消化,不至于被瞬时流量打垮。

六、Docker:把应用装进集装箱(重点)

Docker 把应用和环境打包成标准集装箱

"在我机器上是好的"

每个开发者都说过这句话。你的代码依赖 Node 22、某个版本的数据库客户端、三个环境变量——换台机器就各种报错。根源是应用和运行环境没有绑定在一起

镜像与容器:集装箱比喻

Docker 的核心思想来自航运业:集装箱发明前,货物形状各异、装卸混乱;集装箱把一切标准化,船、火车、卡车都能直接运。

  • 镜像(Image)= 标准集装箱的图纸:把你的代码、Node 运行时、依赖、启动命令全部打包定义好
  • 容器(Container)= 按图纸造出来的集装箱实例:在哪台机器上打开,里面都一模一样

从此部署不再是"在服务器上装环境",而是"把集装箱吊上去"。

Dockerfile:集装箱的装箱清单

一个 Node/Next.js 项目的最小 Dockerfile 长这样:

FROM node:22-alpine          # 基础镜像:带 Node 22 的精简 Linux
WORKDIR /app                 # 工作目录
COPY package*.json ./        # 先拷依赖清单(利用构建缓存)
RUN npm ci                   # 安装依赖
COPY . .                     # 再拷全部代码
RUN npm run build            # 构建
EXPOSE 3000                  # 声明端口
CMD ["npm", "start"]         # 启动命令

每一行是一条装箱指令。docker build 按清单造出镜像,docker run 启动容器——任何装了 Docker 的机器上,行为完全一致。

docker-compose:一键起一整套

真实应用不止一个容器:app、数据库、缓存可能要一起起。docker-compose 用一个 YAML 文件描述整套服务:

services:
  app:
    build: .
    ports: ["3000:3000"]
    depends_on: [db]
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: example

docker compose up 一条命令,应用和数据库同时就绪——新同事入职第一天就能跑起整个项目,这就是它的价值。

再往前一步,CI/CD 流水线里的"构建镜像 → 推到镜像仓库 → 服务器拉取重启",本质也是同一套集装箱逻辑的自动化。学会了 Docker,你学到的不是一个工具,而是现代交付的通用语言。

七、上线与安全基础

上线前最后一块拼图,每一点都值得单独深学,这里先建立意识:

  • HTTPS:明文 HTTP 在传输链路上任何人都能看、能改。用 Let's Encrypt 免费证书 + 强制跳转,是今天的底线配置,没有之一。
  • 鉴权:Session 是"服务器记账"(给你一张会员卡,店里记着你是谁),JWT 是"自带防伪的凭证"(服务器不用查记录,验签名就认人)。小项目 Session 简单直接,多端/微服务场景 JWT 更灵活。
  • SQL 注入:永远不要把用户输入拼进 SQL 字符串。攻击者在输入框里写 '; DROP TABLE users;-- 的故事每天都在发生。参数化查询(占位符传参)是唯一的正确姿势,所有现代数据库库默认支持。
  • 限流:给接口加频率限制(如每 IP 每分钟 60 次),防爬虫、防恶意刷接口、防自己的前端出 bug 打死服务器。

八、学习路径建议与小结

后端的知识树很大,但入门路径可以很朴素:

  1. 先跟完一次请求的一生,把 DNS/TCP/HTTP 的链路图画在纸上
  2. 用 SQL 写一个小项目(记账本、待办清单),亲手建表、加索引、体会事务
  3. 制造一次超卖:写个并发脚本同时请求你的接口,亲眼看库存变负数,再亲手用乐观锁修掉它——这一次实验胜过读十篇文章
  4. 用 Docker 把你的项目打包上线,体验"一次构建,到处运行"

回头看,后端的核心心智模型就四个:请求链路是地图,数据库是地基,并发与幂等是质量的分水岭,Docker 是交付的标准化。剩下的 Redis、队列、微服务,都是这四块积木之上的组合。把这篇文章的概念逐个动手验证一遍,你就已经越过了后端学习最陡的那段坡。

相关教程

后端从零开始:一次请求的一生、并发、缓存与 Docker 打包 | AIHub