|
概念 |
一句话说明 |
解决什么问题 |
|
Event Loop |
Node.js 单线程处理并发的核心机制 |
为什么 Node 能处理大量并发请求 |
|
Stream/Buffer |
流式处理数据,不需要一次性加载到内存 |
处理大文件、实时数据 |
|
中间件 (Middleware) |
请求进来到响应出去之间的处理层 |
日志、鉴权、数据转换等横切关注点 |
|
ORM |
用代码对象操作数据库,不写 SQL |
数据库操作的抽象层 |
|
连接池 |
预建一批数据库连接复用 |
避免每次请求都创建/销毁连接 |
|
鉴权 (Auth) |
验证"你是谁"+ 判断"你能做什么" |
系统安全、权限管理 |
|
JWT |
JSON Web Token,无状态的身份令牌 |
分布式系统中免 Session 的身份验证 |
|
OAuth 2.0 |
第三方授权协议(微信登录、GitHub 登录) |
让用户用已有账号授权第三方应用 |
|
RBAC |
基于角色的访问控制 |
不同角色有不同操作权限 |
|
Redis |
内存数据库,极快 |
缓存、分布式锁、消息队列、排行榜 |
|
缓存策略 |
TTL/LRU/Cache-aside/Write-through |
减少数据库压力、提升响应速度 |
|
消息队列 |
异步解耦,生产者发消息/消费者处理 |
解耦服务、削峰填谷、异步处理 |
|
定时任务 |
Cron 式周期执行 |
定期清理、报表生成、数据同步 |
|
事务 (Transaction) |
一组操作要么全成功要么全回滚 |
数据一致性(转账必须扣减+增加同时成功) |
|
索引 |
数据库中对列建快速查找结构 |
查询加速(类似书的目录) |
|
SQL vs NoSQL |
关系型 vs 文档型/键值型 |
不同数据模型适合不同场景 |
大白话讲解:
1.Event Loop(事件循环)
一句话:
Event Loop 就是 Node.js 的"调度员",它让一个人(单线程)能同时服务很多客人
打比方:
想象你是一个奶茶店唯一的店员。
-
客人 A 点了一杯奶茶,你把订单丢给后厨机器(等水烧开、加料),然后你不傻等。
-
客人 B 来了,你接单,同样丢给后厨。
-
客人 C 来了,继续接单。
-
后厨喊"A 号奶茶好了!",你拿过来给客人 A。
-
后厨喊"C 号好了!",你拿给客人 C。
你始终只有一个人,但因为你不傻等每杯奶茶做完,而是"接单→丢出去→接下一单→有结果了再处理",所以能同时服务很多客人。
Event Loop 就是你脑子里那个循环:不断检查"有没有新客人?有没有做好的奶茶?"
它解决什么问题:
传统方式(Java / PHP 早期):每来一个请求就开一个线程(雇一个店员)。1000 个请求 = 1000 个线程,很贵、很重。
Node.js 的方式:一个线程(一个店员)+ Event Loop(调度循环),遇到耗时操作(数据库查询、文件读写、网络请求)就"丢出去等回调",自己继续处理下一个请求。
所以 Node.js 特别适合 I/O 密集型 的场景(API 服务器、聊天应用),因为大部分时间在等数据库/网络,Event Loop 可以在等的时候干别的事。
不适合的场景:CPU 密集型(比如图片处理、大量计算)。因为你只有一个店员,如果一杯奶茶需要你亲自手捏10分钟,其他客人就得干等。这时候就需要 Worker Threads(临时雇个人帮忙算)。
与前端关系:浏览器的 JS 也有 Event Loop(setTimeout、Promise、requestAnimationFrame 的执行顺序就是它管的)。你已经无意识地在用它了,只是 Node.js 里多了文件 I/O 和网络 I/O 的场景
2.Buffer(缓冲区)
一句话:
Buffer 就是一块固定大小的内存空间,专门用来存二进制数据(0 和 1)
打比方:
你搬家,Buffer 就是一个纸箱。不管你要搬的是书、衣服还是碗,都先装进纸箱(二进制格式),到了新家再拆出来
它解决什么问题:
JavaScript 原生只擅长处理文字(字符串),但后端经常要处理图片、视频、文件、网络数据包这些二进制内容。Buffer 就是 Node.js 给 JS 补上的"处理二进制"的能力。
前端的你已经碰过类似的东西:
- Blob(二进制大对象)
- ArrayBuffer
- FileReader
Buffer 就是 Node.js 版的 ArrayBuffer,但用起来更方便。
3.Stream(流)
一句话:
Stream 是一点一点处理数据的方式,不需要把整个东西一次性加载到内存里
打比方:
没有 Stream:你要看一部 2GB 的电影,先整个下载到手机(占 2GB 内存),然后才能看。
有 Stream:你边下边看(流媒体),手机里同一时刻只有几 MB 的数据在内存中。
解决什么问题:
假设你的 API 要返回一个 500MB 的文件给用户:
❌ 不用 Stream:读取整个文件到内存 → 内存占用 500MB → 再发给用户
10个人同时下载 = 5GB 内存,服务器直接炸
✅ 用 Stream:一边从硬盘读一小块 → 一边发给用户 → 内存始终只占几 MB
10个人同时下载也才几十 MB 内存
四种Stream:
| 类型 | 干嘛的 | 类比 |
|---|---|---|
| Readable | 数据流出来的源头 | 水龙头 |
| Writable | 数据流进去的目的地 | 水桶 |
| Duplex | 可读也可写 | 电话(双向通话) |
| Transform | 读进来 → 变换 → 写出去 | 净水器(脏水进、干净水出) |
你已经在用Stream了:
前端:
- fetch() 的 response.body → 就是 ReadableStream
- AI 聊天的流式输出(SSE)→ 本质就是 Stream
后端:
- 文件读写:fs.createReadStream() / fs.createWriteStream()
- HTTP 请求/响应:req 是 Readable,res 是 Writable
- 文件上传:你项目的分片上传,后端接收的就是 Stream
4.Stream 和 Buffer 的关系
一句话:
Stream 是水管(持续流动的数据),Buffer 是水桶(临时存一坨数据)
打比方:
水管里的水不是一口气全来的,所以需要水桶先接一点、处理一点、再接一点。Node.js 的 Stream 内部就是用 Buffer 来暂存每一小块数据的。
一句话总结:
Buffer = 装二进制数据的容器,Stream = 一点一点处理数据的管道。解决的核心问题是内存效率——不需要把整个文件/数据一次性塞进内存。
5.中间件(Middleware)
一句话:
中间件就是请求到达你的业务代码之前,经过的一道道安检关卡。
打比方:
你去机场坐飞机,从进门到登机,要经过的每一步都是一个中间件
每一道关卡就是一个中间件:
-
测体温:发烧的人直接拦住,不让进(异常处理中间件)
-
查身份证:确认你是谁(鉴权中间件)
-
安检:检查你带了啥(数据校验中间件)
-
查登机牌:你有没有权限上这班飞机(权限中间件)
每道关卡只管自己那件事,检查完了就放行给下一道。任何一道关卡不通过,直接打回去,后面的关卡和飞机(业务代码)都不会执行。
前端类比:
React Router 的路由守卫 / 导航守卫
→ 用户没登录?跳转到登录页(相当于鉴权中间件)
Axios 的请求拦截器 / 响应拦截器
→ 每个请求自动加上 Token header(相当于鉴权中间件)
→ 每个响应自动检查 401 状态码(相当于错误处理中间件)
一句话总结:
中间件 = 业务代码之前/之后的通用处理层。解决的核心问题是代码复用——把鉴权、日志、校验这些每个接口都要做的事情,提出来只写一次
6.ORM(Object-Relational Mapping,对象关系映射)
一句话:
ORM 让你用写代码的方式操作数据库,不用写 SQL。
打比方:
你要从图书馆找一本书。
❌ 不用 ORM(直接写 SQL):
你得学图书馆的编目语言,写一张检索单:
"SELECT * FROM books WHERE author = '鲁迅' AND year > 1925 ORDER BY year DESC"
→ 你得懂 SQL 语法、表名、字段名、JOIN 关系...
✅ 用 ORM(写代码):
你跟图书管理员(ORM)说:
Book.findMany({ where: { author: '鲁迅', year: { gt: 1925 } }, orderBy: { year: 'desc' } })
→ 管理员帮你翻译成 SQL 去查,你只需要用你熟悉的编程语言。
它解决什么问题:
-
不用学 SQL:用 TypeScript/JavaScript 语法操作数据库
-
类型安全:Prisma 能自动生成类型,写错字段名 IDE 直接报红
-
防 SQL 注入:ORM 自动处理参数转义,不怕恶意输入
-
换数据库方便:代码不变,改个配置就能从 MySQL 切到 PostgreSQL
一句话总结:
ORM = 数据库的翻译官,你写 TypeScript,它翻译成 SQL 去查数据库。解决的核心问题是让后端开发者用编程语言操作数据库,不用手写 SQL,还能享受类型安全。
7.连接池(Connection Pool)
一句话:
预先建好一批数据库连接放那儿,谁需要谁拿去用,用完归还,不用每次都重新建连接。
打比方:
你是公司前台,经常需要打电话给不同客户。
❌ 没有连接池:
每次要打电话 → 申请一条新电话线 → 打完电话 → 拆掉电话线
下次又要打 → 再申请一条 → 再拆掉
100个人同时要打电话 = 同时拉100条电话线 = 费时费钱
✅ 有连接池:
公司一开始就装了 10 条电话线(连接池 size = 10)
前台 A 要打电话 → 拿起1号线 → 打完放回去
前台 B 要打电话 → 拿起2号线 → 打完放回去
前台 C 要打电话 → 1号线空了 → 拿起1号线复用
如果 10 条线全占满了 → 第 11 个人排队等(排队超时就报错)
它解决什么问题:
建一t条数据库连接是很贵的操作:
建立连接要做的事:
1. TCP 三次握手(网络来回)
2. 数据库验证用户名密码
3. 分配内存、创建会话
4. 可能还有 SSL 握手
一次建立连接 ≈ 几十~几百毫秒
你的 API 响应要求 < 100ms
如果每个请求都重新建连接,光建连接就超时了
连接池的解决方式:
服务启动时 → 预建 5~20 个连接(放在池子里保持活跃)
请求来了 → 从池子里借一个 → 执行 SQL → 归还到池子
借: < 1ms(已经建好的连接,直接拿)
vs
新建: 50~200ms(每次都重新握手)
关键参数:
min: 5 → 最少保持 5 个连接(即使没人用也留着,避免冷启动)
max: 20 → 最多 20 个连接(超了就排队,防止把数据库压垮)
idle: 30s → 空闲超过 30 秒的连接回收(省资源)
timeout: 5s → 排队超过 5 秒还没拿到连接就报错(避免用户干等)
和前端类比:
你其实用过类似概念:
HTTP 连接的 keep-alive:
浏览器和服务器之间不是每次请求都重新建 TCP 连接
而是一条连接用完不关,下次请求复用
→ 这就是"HTTP 连接池"
在Prisma里怎么配置:
# .env 文件
DATABASE_URL="postgresql://user:pass@localhost:5432/mydb?connection_limit=10"
# connection_limit=10 就是连接池大小
# Prisma 自动管理连接的借和还,你不用操心
一句话总结:
连接池 = 预建一批数据库连接复用,避免每次请求都重新建连接。解决的核心问题是性能——把几百毫秒的建连接开销降到不到 1 毫秒。
8.鉴权 (Authentication & Authorization)
一句话:
鉴权回答两个问题——你是谁(认证)+ 你能做什么(授权)。
打比方:
先认证,再授权。不知道你是谁,就没法判断你能做什么。
你去公司上班。
认证(Authentication)= 门口刷工牌
→ 确认"你是张三",不是冒充的
授权(Authorization)= 你能进哪些房间
→ 张三是普通员工,能进办公室,不能进服务器机房
→ 李四是运维,能进服务器机房
→ 王五是老板,哪都能进
认证(你是谁?)
三种常见方式:
a.Session/Cookie(传统方式)
登录流程:
用户提交 账号+密码 → 服务器验证通过 → 服务器创建一个 Session(会话记录)
→ 把 Session ID 塞到 Cookie 里返回给浏览器
后续请求:
浏览器自动带上 Cookie(含 Session ID)→ 服务器查 Session ID
→ 找到了 = 知道你是谁 → 放行
→ 找不到 = 没登录或过期 → 401
特点:
状态保存在服务器(Session 存在内存或 Redis 里)
服务器要记住"谁在线"
b.JWT(现在主流)
登录流程:
用户提交 账号+密码 → 服务器验证通过 → 服务器生成一个 JWT Token
→ Token 本身就包含了用户信息(id, role, 过期时间)
→ 用密钥签名,防篡改 → 返回给前端
后续请求:
前端在 Header 里带上:Authorization: Bearer eyJhbGciOi...
→ 服务器验证签名 → 签名对的 = 没被篡改 → 解析出用户信息
→ 不需要查数据库!Token 自带用户信息
特点:
无状态 — 服务器不需要记住"谁在线"
适合分布式 — 多台服务器都能验证同一个 Token
c. OAuth 2.0(第三方登录)
你在各种网站看到的:"用微信登录""用 GitHub 登录""用 Google 登录"
流程(简化版):
用户点"用 GitHub 登录" → 跳转到 GitHub 页面
→ 用户在 GitHub 确认授权 → GitHub 给你的服务器一个授权码
→ 你的服务器拿授权码去 GitHub 换 Token
→ 用 Token 获取用户的 GitHub 信息(头像、邮箱等)
→ 在你的系统里创建/关联账号 → 登录成功
本质:
你不需要用户的 GitHub 密码
GitHub 只告诉你"这个人确实是 xxx"
前后端举例:
前端做的是"鉴权的前半段":
存 Token → 带 Token → 401 时跳登录 → 根据权限显示/隐藏 UI
后端做的是"鉴权的后半段":
验 Token → 解析用户 → 检查权限 → 允许/拒绝 API 调用
一句话总结:
认证 = 确认"你是谁"(通常用 JWT),授权 = 判断"你能做什么"(通常用 RBAC)。
9.JWT(JSON Web Token)
一句话:
JWT是一张自带身份信息的通行证,服务器验签名就知道你是谁,不用查数据库
打比方:
Session 方式(传统):
你去游乐园,入口给你一个手环,上面只有一个编号 "A1234"
每次玩项目,工作人员要打电话给入口:"A1234 是谁?能玩这个吗?"
入口翻花名册查一下,再回复
→ 每次都要查中央记录(数据库)
JWT 方式:
你去游乐园,入口给你一张卡,上面写着:
"张三 | VIP | 有效期至今天18:00 | 入口盖章✓"
每次玩项目,工作人员只需要看:
1. 卡上写了啥 → 张三,VIP
2. 盖章是不是真的 → 验证入口的印章(签名)
3. 有没有过期 → 看有效期
→ 不用打电话给任何人,卡自带所有信息
JWT长什么样:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcxMH0.abc123signature
用 . 分成三段:
第一段(Header): eyJhbGciOiJIUzI1NiJ9
解码后 = { "alg": "HS256" }
→ 说明用什么算法签名
第二段(Payload): eyJ1c2VySWQiOjEsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcxMH0
解码后 = { "userId": 1, "role": "admin", "exp": 1710000000 }
→ 用户信息 + 过期时间(这就是"卡上写的内容")
第三段(Signature): abc123signature
→ 用密钥对前两段做签名(这就是"入口盖的章")
注意:前两段只是 Base64 编码,任何人都能解码看到内容。安全性不在于"看不到",而在于**“改不了”**——改了内容,签名就对不上。
完整流程:
1. 登录
前端: POST /api/login { username: "张三", password: "123" }
后端: 验证密码 → 正确 → 生成 JWT:
payload = { userId: 1, role: "admin", exp: 24小时后 }
token = sign(payload, "服务器密钥")
→ 返回 { token: "eyJhbG..." }
2. 前端存 Token
localStorage / Cookie / 内存 → 存起来
3. 后续请求自动带 Token
GET /api/resources
Headers: { Authorization: "Bearer eyJhbG..." }
4. 后端验证 Token
收到请求 → 取出 Authorization header → 提取 Token
→ verify(token, "服务器密钥") → 签名对吗?过期了吗?
→ 对的 → 解析出 { userId: 1, role: "admin" } → 继续处理
→ 不对 → 返回 401
5. 前端处理 401
Axios 响应拦截器 → 收到 401 → 清除 Token → 跳转登录页
Session vs JWT对比:
| Session | JWT | |
|---|---|---|
| 信息存哪 | 服务器(内存/Redis) | Token 自己带着 |
| 每次验证 | 查数据库/Redis | 只验签名,不查库 |
| 分布式 | 麻烦(多台服务器要共享 Session) | 简单(每台都能验证) |
| 注销 | 删掉 Session 就行 | 麻烦(Token 发出去收不回来) |
| 安全性 | 服务器控制 | 需要额外处理(黑名单/短过期 |
JWT 最适合:前后端分离、微服务架构、移动端 APP
JWT的坑:
1. Token 泄露 = 身份被盗(所以要用 HTTPS、短过期时间)
2. 无法主动让 Token 失效(可用 Redis 黑名单方案弥补)
3. Payload 不要放敏感信息(因为任何人都能解码)
4. Token 太大 → 每个请求都带着 → 增加带宽(别往里塞太多东西)
一句话总结:
JWT = 一张自带身份信息 + 防伪签名的通行证。服务器不用查数据库,验签名就知道"你是谁、你能做什么、有没有过期"。你前端每天在带着 JWT 发请求,学后端就是学怎么签发和验证这张通行证。
10.OAuth 2.0
一句话:
OAuth 2.0 是一个**“让用户用别人家的账号登录你的应用”**的标准协议。
打比方:
你要入住一家酒店,但酒店说:“我们不发房卡,你去物业那刷个脸,物业给你临时通行证。”
你(用户)→ 酒店(你的应用)→ 物业(GitHub/微信/Google)
1. 酒店前台说:"请先去物业验证身份"
2. 物业问你:"酒店想获取你的姓名和头像,你同意吗?"
3. 你点"同意"
4. 物业给酒店一把临时钥匙(Access Token)
5. 酒店拿这把钥匙能看你的姓名和头像,但看不了你的银行卡信息
6. 钥匙有有效期,过期了酒店就用不了了
完成流程(授权码模式,最常用):
参与者:
用户 = 你
客户端 = 你的应用
授权服务器 = GitHub/微信/Google 的登录页
资源服务器 = GitHub/微信/Google 存用户信息的服务器
Step 1 用户点击"用 GitHub 登录"
→ 前端跳转到:
https://github.com/login/oauth/authorize
?client_id=你的应用ID
&redirect_uri=https://你的网站.com/callback
&scope=user:email ← 想要获取的权限范围
Step 2 用户在 GitHub 页面看到:
"XX应用 想要获取你的邮箱和头像,是否同意?"
→ 用户点"同意"
Step 3 GitHub 把用户跳转回你的网站:
https://你的网站.com/callback?code=abc123
→ 这个 code 就是"授权码",一次性的,几分钟过期
Step 4 你的后端拿着 code 去 GitHub 换 Token:
POST https://github.com/login/oauth/access_token
{ client_id, client_secret, code: "abc123" }
→ GitHub 返回:{ access_token: "gho_xxxx" }
Step 5 你的后端拿 access_token 去获取用户信息:
GET https://api.github.com/user
Headers: { Authorization: "Bearer gho_xxxx" }
→ GitHub 返回:{ name: "张三", email: "zhang@...", avatar: "..." }
Step 6 你的后端:
→ 查数据库有没有这个 GitHub 用户
→ 没有 → 创建新账号
→ 有 → 关联已有账号
→ 签发你自己系统的 JWT → 返回给前端 → 登录完成
关键概念:
| 概念 | 是什么 |
|---|---|
| client_id | 你的应用在 GitHub 注册时得到的 ID(公开的) |
| client_secret | 你的应用的密钥(只有后端知道,绝不能暴露给前端) |
| authorization code | 一次性授权码,用来换 Token(防止 Token 在 URL 里泄露) |
| access_token | GitHub 给你的临时通行证,能读取用户信息 |
| refresh_token | access_token 过期后用它换新的(不用用户重新授权) |
| scope | 权限范围:user:email 只能读邮箱,repo 能读仓库 |
和JWT的关系:
OAuth 2.0 和 JWT 不是同一层的东西:
OAuth 2.0 = 协议/流程(怎么获取授权)
JWT = 令牌格式(Token 长什么样)
它们经常配合使用:
OAuth 流程获取授权 → 拿到用户信息 → 你的系统签发 JWT → 后续用 JWT 认证
你项目的流程大概是:
OAuth 第三方登录 → 获取用户信息 → 你的后端签发 JWT → 前端存 JWT → 后续带 JWT 访问 API
一句话总结:
OAuth 2.0 = 一套标准流程,让用户安全地用第三方账号(GitHub/微信/Google)登录你的应用,你的应用永远不碰用户的第三方密码。核心是"授权码换 Token"这个安全交换过程。
11.RBAC(基于角色的访问控制)
一句话:
RBAC 就是"什么角色的人能做什么事",给人分角色比给每个人单独配权限省事多了。
解决什么问题:
- 管理简单:500 人的权限只需管 5 个角色
- 一致性:同一角色的人权限完全一样,不会出现"同样是编辑,张三能删除李四不能"
- 易变更:调岗 = 换个角色;新增权限 = 改角色定义
- 审计方便:谁有什么权限一目了然
前后端各自做什么:
前端(你做的):
用户登录 → 拿到角色/权限列表 → 根据权限显示/隐藏 UI
→ 体验优化,不是安全保障(因为前端代码用户可以篡改)
后端(你要学的):
每个 API 请求 → 中间件检查角色 → 有权限才执行,没有返回 403
→ 真正的安全关卡
前端 RBAC = 门上贴的"仅员工进入"的牌子
后端 RBAC = 门上装的门禁系统
两个都要有,但真正挡人的是后端
一句话总结:
RBAC = 给人分角色,角色绑权限。解决的核心问题是大规模权限管理——不用给每个人单独配权限,只需管几个角色就够了。
12.Redis
一句话:
Redis 是一个把数据存在内存里的数据库,快到飞起,但重启会丢数据(默认情况下)。
打比方:
普通数据库(MySQL/PostgreSQL) = 图书馆的书架
→ 数据存在硬盘上(书架),很安全,掉电不丢
→ 但每次查数据要去书架翻书,有点慢
Redis = 你桌上的便利贴
→ 数据存在内存里(贴在桌上),拿起来就看到,超级快
→ 但桌子推倒了(重启/断电),便利贴可能掉了
速度差异:硬盘读取 ~几毫秒,内存读取 ~几微秒。Redis 比普通数据库快 几十到几百倍。
Redis干什么用的:
1.缓存(最常见,占80%用途)
场景:你的首页要显示"热门资源 Top 10"
❌ 没有缓存:
每个用户访问首页 → 查数据库 → 排序 → 返回
1000 人同时访问 = 查 1000 次数据库
数据库表示:我顶不住了
✅ 有 Redis 缓存:
第 1 个用户访问 → 查数据库 → 结果存一份到 Redis → 返回
第 2~1000 个用户访问 → 直接从 Redis 取 → 不碰数据库
每 5 分钟更新一次 Redis 里的数据
数据库表示:清闲~
你可以理解为:Redis 就是数据库和用户之间的一块高速缓冲区。先查 Redis,有就直接用;没有再查数据库,查完顺便存一份到 Redis。
2.Session存储
用户登录后的 Session 数据存在 Redis 里
→ 多台服务器都能查到同一个 Session
→ 解决集群环境下的登录状态共享问题
3.分布式锁
场景:100 个人同时抢购最后 1 件商品
❌ 没有锁:100 个请求同时读到"还有 1 件"→ 都以为能买 → 超卖了
✅ 有 Redis 锁:第 1 个请求锁住 → 扣库存 → 解锁 → 后面的人再来
用 Redis 做锁是因为它足够快(微秒级),不会成为瓶颈
4.消息队列(简单版)
Redis 有 Pub/Sub(发布/订阅)功能
发送方发一条消息 → Redis → 所有订阅者收到
实时通知、聊天消息广播等场景
(正式项目通常用专业队列 RabbitMQ/Kafka,Redis 做轻量级的)
5.排行榜/计数器
Redis 的 Sorted Set 天然适合排行榜:
ZADD leaderboard 100 "张三"
ZADD leaderboard 85 "李四"
ZREVRANGE leaderboard 0 9 → Top 10
点赞数、访问量这种高频计数也适合放 Redis
→ 频繁 +1 操作在内存里做,不用每次写硬盘数据库
Redis和数据库的关系
Redis ≠ 数据库的替代品
Redis = 数据库的好搭档
正确用法:
用户请求 → 先查 Redis(快)
→ Redis 有 → 直接返回
→ Redis 没有 → 查数据库(慢)→ 结果存 Redis → 返回
数据存储:主数据库
速度加速:Redis
就像电脑:
硬盘 = 主数据库(大、安全、慢)
内存 = Redis(小、快、掉电会丢)
CPU 先查内存,内存没有再从硬盘加载到内存
和前端类比:
你其实已经在用"前端版 Redis"了:
localStorage / sessionStorage
→ 把数据缓存在浏览器里,不用每次都请求 API
React state / zustand store
→ 把 API 返回的数据存在内存里,组件直接读
→ 和 Redis 在后端做的事一样:减少对数据源的请求
一句话总结:
Redis=内存数据库=极速便利贴。核心用途是缓存——把热数据存在内存里,让用户少等几百毫秒,数据库少扛几千倍压力。数据库存"真"数据,Redis 存"快"数据。
13.缓存策略
一句话:
缓存策略回答两个问题——什么时候存,什么时候删
问题1:缓存什么时候存?
a.Cache-aside(旁支缓存)——最常用
读数据:
查 Redis → 有(缓存命中)→ 直接返回
→ 没有(缓存未命中)→ 查数据库 → 存一份到 Redis → 返回
写数据:
写入数据库 → 删掉 Redis 里的旧缓存
下次有人读 → 缓存没了 → 查数据库 → 重新存 Redis
核心思路:应用自己负责"存"和"删",Redis 只是个仓库。
打比方:你去食堂打饭。货架上有就拿(命中),没有就找后厨做,做好了顺便往货架上放一份(回填缓存)
b.Read-through / Write-through(读穿/写穿)
读数据:
查 Redis → 没有 → Redis 自己去查数据库 → 存好 → 返回给你
写数据:
写 Redis → Redis 自己同步到数据库
核心思路:Redis 是"中间人",帮你自动管数据库同步。
打比方:你跟中介说"帮我找房子",中介帮你找好直接给你,你不用自己上链接。
c.Write-behind(异步写回)
写数据:
先写 Redis(快)→ 过一会儿再批量同步到数据库
优点:写入超快(只写内存)
缺点:如果 Redis 挂了,没来得及同步的数据就丢了
适合:日志收集、计数器等"丢一点也无所谓"的场景
问题2:缓存什么时候删?(过期策略)
a.TTL(Time To Live,存活时间)
存缓存时设一个倒计时:
SET user:1 "{name:张三}" EX 3600 // 3600秒=1小时后自动过期
1小时后 Redis 自动删掉这条数据
下次有人读 → 缓存没了 → 重新查数据库
用途:大部分场景都用 TTL,简单有效
打比方:便利贴上写个日期,到期就撕掉。
b.LRU(Least Recently Used,最近最少使用)
Redis 内存满了怎么办?
→ 把最久没人用过的数据踢掉,腾出空间给新数据
热门数据一直被访问 → 一直留着
冷门数据没人读 → 淘汰
用途:Redis 内存有限时的淘汰策略
打比方:你桌子只能放 10 张便利贴,满了就把最久没看的那张扔掉。
c.LFU(Least Frequently Used,最不常使用)
统计每条数据被访问的次数
内存满了 → 踢掉访问次数最少的
和 LRU 的区别:
LRU 看"最后一次什么时候用的"
LFU 看"总共用了几次"
缓存的三大经典问题
① 缓存穿透
问题:有人故意查一个不存在的数据(比如 id=-1)
Redis 没有 → 查数据库 → 数据库也没有 → Redis 还是没缓存
反复请求 → 每次都穿透到数据库 → 数据库被打爆
解决:
方案 A:查到数据库没有 → 在 Redis 存个空值 SET user:-1 "null" EX 60
方案 B:布隆过滤器(快速判断"这个 id 绝对不存在")
② 缓存击穿
问题:某个超级热门的 key(比如首页推荐)过期了
瞬间 10000 个请求同时来 → 全部穿透到数据库 → 数据库被打爆
解决:
方案 A:热门 key 永不过期 + 后台定时更新
方案 B:加分布式锁,只让一个请求去查数据库,其他人等着
③ 缓存雪崩
问题:大量 key 在同一时刻过期(比如都设了 TTL=3600)
→ 所有请求瞬间涌向数据库
解决:
TTL 加随机偏移:不是都 3600 秒,而是 3600 ± 随机 300 秒
→ 过期时间分散开,不会同时过期
你做前端时其实也在处理缓存策略:
HTTP 缓存:
Cache-Control: max-age=3600 → 就是 TTL!浏览器缓存 1 小时
ETag / If-None-Match → 服务器判断"内容变了吗",没变就用缓存
React 缓存:
useMemo / useCallback → 计算结果缓存,依赖没变就不重算
SWR / React Query:
"stale-while-revalidate" → 先用旧缓存显示,后台悄悄刷新
→ 这就是缓存策略在前端的高级应用
一句话总结:
缓存策略 = 存什么、存多久、满了怎么办、出问题怎么办。最常用的组合是"Cache-aside + TTL + LRU"——手动存删 + 定时过期 + 内存满了踢冷数据。本质和你做前端的 HTTP 缓存、useMemo 是一个道理。
14.消息队列(Message Queue)
一句话:
消息队列就是一个待办事项清单——生产者往里扔任务,消费者从里面取任务做。
打比方:
你去奶茶店点奶茶:
❌ 没有消息队列(同步处理):
你到柜台 → 店员现场做 → 你站着等 5 分钟 → 做好了给你 → 下一个顾客才能点单
后面排了 50 个人 → 大家都在干等
✅ 有消息队列(异步处理):
你到柜台 → 店员写张小票贴到"待做墙"上 → 给你一个取餐号 → 你先去坐着
后厨看到墙上小票 → 按顺序做 → 做好了叫号
你不用站着等,后面的人也能继续点单
"待做墙" = 消息队列
"小票" = 消息
"柜台店员" = 生产者(Producer)
"后厨" = 消费者(Consumer)
解决什么问题:
1.异步结构
场景:用户注册成功后要做 3 件事
❌ 同步处理(不用队列):
用户注册 → 存数据库(50ms) → 发欢迎邮件(2000ms) → 初始化用户空间(1000ms)
用户等了 3 秒才看到"注册成功"
而且邮件服务挂了 → 整个注册流程失败
✅ 异步处理(用队列):
用户注册 → 存数据库(50ms) → 扔3条消息到队列 → 立即返回"注册成功"(50ms!)
消费者A 从队列取消息 → 发邮件(慢慢发,失败了重试)
消费者B 从队列取消息 → 初始化空间
消费者C 从队列取消息 → 发通知
用户 50ms 就看到结果
邮件服务挂了?消息还在队列里,恢复后继续发,不影响注册
2.削峰填谷
场景:双 11 零点,1 秒内涌入 10 万订单
❌ 没有队列:10 万请求直接打到数据库 → 数据库挂了
✅ 有队列:10 万消息先进队列 → 消费者按自己的速度处理(比如每秒 1000 个)
→ 100 秒消化完,数据库稳稳的
队列就像水库:
洪水来了 → 先存水库里 → 慢慢放水 → 下游不被淹
3.服务解耦
场景:订单系统要通知库存系统、物流系统、积分系统
❌ 不用队列:
订单服务直接调用库存API、物流API、积分API
→ 订单服务要知道所有下游服务的地址
→ 任何一个挂了,订单流程就断了
→ 新增一个"数据分析服务"→ 改订单代码
✅ 用队列:
订单服务 → 往队列发一条"订单已创建"消息 → 完事
库存服务 → 订阅这条消息 → 减库存
物流服务 → 订阅这条消息 → 创建物流单
积分服务 → 订阅这条消息 → 加积分
新增数据分析服务 → 也订阅这条消息 → 不用改订单代码
服务之间互不认识,只和队列打交道
跟前端类比:
事件系统(EventEmitter / CustomEvent):
组件 A 触发事件 → 组件 B/C/D 监听并响应
→ 这就是最简单的"发布/订阅"模式 = 消息队列的核心思想
Promise 链 / async-await:
任务 A 完成后触发任务 B → 异步处理
→ 消息队列做的是分布式版的异步处理
Web Worker:
主线程扔任务给 Worker → Worker 处理完回传结果
→ 主线程(生产者) → 消息 → Worker(消费者)
核心术语:
Producer(生产者):发消息的那个服务
Consumer(消费者):处理消息的那个服务
Queue(队列):存消息的地方,先进先出
Topic(主题):消息的分类(订单消息、邮件消息、通知消息)
ACK(确认):消费者处理完了告诉队列"这条处理好了",队列才删掉
Dead Letter(死信):处理失败 N 次的消息,放到"死信队列"人工处理
一句话总结:
消息队列 = 服务之间的异步传话筒。解决三个核心问题:解耦(服务互不认识)、削峰(流量洪峰不打爆数据库)、异步(用户不用等耗时操作)。
15.定时任务(Scheduled Tasks / Cron Jobs)
一句话:
定时任务就是设好闹钟让程序自动干活,到点就执行,不需要人手动触发
解决什么问题:
有些事情需要定时自动做,不能靠用户触发:
1. 数据清理
→ 每天删掉 30 天前的日志、过期验证码、临时文件
→ 不清理 → 数据库越来越大 → 查询越来越慢
2. 报表生成
→ 每天凌晨统计昨日数据:新增用户数、交易额、活跃度
→ 老板早上打开后台就能看到
3. 缓存刷新
→ 每 5 分钟重新计算"热门资源 Top 10"存到 Redis
→ 用户看到的永远是接近实时的数据
4. 数据同步
→ 每小时从第三方 API 拉取最新数据
→ 比如汇率、天气、库存等外部数据
5. 健康检查
→ 每分钟检测依赖的服务是否正常
→ 不正常 → 自动告警
6. 邮件/通知
→ 每天早上 9:00 发送"待处理事项"提醒
→ 每月发账单
Cron 表达式(定时任务的语言):
Cron 不是一个工具,是一种"描述时间规则"的格式
源自 Unix 系统,几乎所有定时任务框架都用它
格式(5位或6位):
秒 分 时 日 月 星期几
例子:
0 0 2 * * * → 每天凌晨 2:00:00
0 */5 * * * * → 每 5 分钟
0 0 9 * * 1-5 → 每周一到周五早上 9:00
0 0 0 1 * * → 每月 1 号零点
0 30 8 * * 1 → 每周一早上 8:30
特殊符号:
* = 每个(每分钟、每小时...)
*/5 = 每隔 5 个
1-5 = 1到5
1,3,5 = 1和3和5
Node.js里怎么使用:
方式 1:node-cron(轻量,适合小项目)
安装:npm install node-cron
用法和 setInterval 类似,但支持 Cron 表达式
方式 2:@nestjs/schedule(NestJS 官方方案)
> 用装饰器写,超级简洁:
@Cron('0 0 2 * * *') // 每天凌晨2点
handleDailyCleanup() {
// 清理过期数据
}
@Interval(60000) // 每60秒
handleHealthCheck() {
// 健康检查
}
方式 3:BullMQ 重复任务
把定时任务当成"重复消息"放进队列
更适合分布式场景(多台服务器时避免重复执行)
定时任务的坑:
1. 重复执行
→ 3 台服务器都部署了定时任务 → 凌晨 2 点都执行了一次 → 数据清了 3 遍
解决:用分布式锁(Redis)保证只有一台执行
2. 任务堆积
→ 上一次还没执行完,下一次又触发了
解决:加锁 or 检查"上次是否完成"
3. 时区问题
→ 服务器在美国 → Cron 写的凌晨 2 点是美国时间
解决:明确指定时区
4. 失败处理
→ 定时任务失败了怎么办?
解决:记录日志 + 告警 + 重试机制
与前端类比:
setInterval / setTimeout:
setInterval(() => fetchData(), 5 * 60 * 1000) // 每5分钟拉数据
→ 这就是最简单的"定时任务"!
→ 只是在浏览器里跑,用户关了页面就没了
你项目可能用到的前端定时场景:
轮询接口:每 N 秒检查上传进度、AI 推理进度
Token 刷新:Token 快过期前自动 refresh
后端定时任务 = 更强大的 setInterval
→ 跑在服务器上 24/7 不停
→ 用 Cron 表达式精确控制时间
→ 有失败重试、日志记录、分布式管理
一句话总结:
定时任务 = 服务器上的闹钟 + 自动执行脚本。用 Cron 表达式描述"什么时候做",框架负责"到点就做"。解决的核心问题是自动化周期性工作——数据清理、报表生成、缓存刷新这些不能靠人手动触发的事。
16.事务 (Transaction)
一句话:
把多个数据库操作打包成"要么全成功,要么全失败"的原子操作
打比方:
你在淘宝下单 → 扣库存 + 扣余额 + 生成订单,是三个动作。事务保证这三个动作要么一起完成,要么一个都不做。就像你签合同,双方都签了才生效,任何一方反悔整份作废。
解决什么问题:
-
银行转账:A 扣了 1000,B 没收到 → 钱凭空消失
-
下单:库存扣了,但订单没生成 → 用户看不到订单但库存少了
-
核心痛点:操作做到一半崩了,数据变得不一致
不用事务: 步骤1: A 账户 -1000 ✅ 成功 步骤2: B 账户 +1000 ❌ 系统崩了 结果: A 少了钱,B 没收到 → 1000 块消失了
用了事务: 开启事务 步骤1: A 账户 -1000 ✅ 步骤2: B 账户 +1000 ❌ 失败 回滚 → A 账户恢复原样,什么都没发生
四个特性 (ACID):
-
原子性 Atomicity:全做或全不做
-
一致性 Consistency:操作前后数据总量不变(钱不会凭空消失)
-
隔离性 Isolation:多人同时操作互不干扰
-
持久性 Durability:成功了就永久保存,不会莫名丢失
一句话总结:
事务 = 打包操作的"安全气囊",保证数据要么完美完成变更,要么完全不动,不会出现"做了一半"的混乱状态。
17.索引(Index)
一句话:
给数据库表的某些列建一个"目录",让查询不用逐行扫描,直接跳转到目标数据
打比方:
一本 800 页的字典,你要找"事务"这个词。没有目录就得从第 1 页翻到第 800 页。有了目录(索引),翻到目录页查"S"开头 → 第 542 页 → 直接翻过去。
解决什么问题:
-
表里有 100 万条数据,查一条要扫描 100 万行 → 慢到崩溃
-
用户搜索、列表筛选、关联查询,每次都全表扫描 → 数据库扛不住
-
核心痛点:数据量一大,没索引的查询就像大海捞针
没有索引: 查询 “SELECT * FROM users WHERE email = ‘test@example.com’” → 数据库逐行扫描 100 万条记录 → 耗时 2 秒
有了索引: 给 email 列建索引 → 数据库直接定位到目标行(B+树查找) → 耗时 2 毫秒(快 1000 倍)
索引不是越多越好:
-
索引占磁盘空间(相当于多了一本目录)
-
每次写入/更新/删除数据,索引也要同步更新 → 写入变慢
-
就像字典目录太多(按拼音、按笔画、按部首…),新增一个字要更新所有目录
-
原则:读多写少的字段加索引,频繁更新的字段慎加
常见索引类型:
-
主键索引:每张表自动有,就是 id 列,唯一且不为空
-
唯一索引:保证这列不能有重复值(如 email)
-
组合索引:多个列组合建索引(如 app_id + model_id 一起查)
-
全文索引:支持文本内容搜索(如搜索文章内容)
一句话总结:
索引 = 数据库的"字典目录",用空间和写入速度换取查询速度的飞跃
18.SQL vs NoSQL
SQL(关系型数据库):
像严格的 Excel,格式固定,查起来快,数据之间关系明确。
打比方:
想象一个公司的员工花名册:每一行是一个人,每一列是姓名、工号、部门、薪资……
- 表格结构提前定死:有哪些列、每列存什么类型,全部事先规定好
- 每个人的信息必须完整:没有工号不行,部门不能瞎填
- 查数据很方便:“帮我找出技术部薪资大于1万的所有人” → 一句话搞定
- 适合:数据之间有关系的场景(订单关联用户、用户关联地址……)
NoSQL(非关系型数据库):
像自由的文件夹,格式随意,扩展方便,适合大量杂乱数据。
打比方:
想象你有一个文件夹,里面随便丢各种文件:有 Word、有图片、有 JSON……
-
没有固定格式:每条数据可以长得不一样,这条有 10 个字段,那条可以只有 3 个
-
随便存:今天加个字段、明天删个字段,不用改表结构
-
数据量特别大的时候表现更好(几亿条数据)
-
适合:格式不固定、变化快、数据量巨大的场景(聊天记录、日志、用户行为……)
什么时候用 SQL:
-
数据之间有大量关系(用户 → 订单 → 商品 → 分类)
-
需要事务保障(转账、支付)
-
结构稳定、查询复杂(多表关联、聚合统计)
-
你的 CMS 项目的核心数据:app、model、content、fields → 典型 SQL 场景
什么时候用 NoSQL:
-
数据结构经常变(今天 5 个字段,明天 20 个)→ 你的低代码表单动态字段!
-
海量读写、对速度极致要求(缓存、日志、会话)→ Redis
-
全文搜索(搜索内容、模糊匹配)→ Elasticsearch
-
实时数据流、消息队列 → 不需要复杂关系

2837

被折叠的 条评论
为什么被折叠?



