Node.js & MongoDB

高频版 · 全栈联调能力储备

1 Node 核心机制

Node.js 的事件循环和浏览器有什么区别?
浏览器事件循环:一轮 macro → 清空 micro → 渲染(rAF → Style → Layout → Paint)→ 下一轮。
Node 事件循环(libuv):分 6 个阶段轮转:timers → pending callbacks → idle/prepare → poll → check → close callbacks
   ┌───────────────────────────┐
┌──>│         timers            │  setTimeout / setInterval 回调
│  └────────────┬──────────────┘
│  ┌────────────▼──────────────┐
│  │     pending callbacks     │  系统级回调(TCP 错误等)
│  └────────────┬──────────────┘
│  ┌────────────▼──────────────┐
│  │       idle, prepare       │  内部使用
│  └────────────┬──────────────┘
│  ┌────────────▼──────────────┐
│  │          poll             │  I/O 回调(fs.read、网络请求)
│  └────────────┬──────────────┘
│  ┌────────────▼──────────────┐
│  │          check            │  setImmediate 回调
│  └────────────┬──────────────┘
│  ┌────────────▼──────────────┐
│  │      close callbacks      │  socket.on('close') 等
│  └────────────┬──────────────┘
└───────────────┘

关键差异:

  • process.nextTick 在每个阶段切换前清空,优先级高于 Promise.then
  • setImmediate 在 check 阶段执行,setTimeout(fn, 0) 在 timers 阶段,两者在 I/O 回调内 setImmediate 必先执行
  • Node 没有渲染阶段,不存在 rAF
Node 是单线程的吗?为什么能处理高并发?
JS 执行是单线程,但底层 libuv 维护了一个线程池(默认 4 个线程,可通过 UV_THREADPOOL_SIZE 调整)处理阻塞 I/O(文件读写、DNS 解析、crypto)。网络 I/O 走 epoll/kqueue 等操作系统异步机制,不占线程池。
  • 事件驱动 + 非阻塞 I/O:请求进来 → 注册回调 → 事件循环处理 → 回调触发响应,不会为每个请求创建线程
  • CPU 密集型任务会阻塞事件循环 → 解决方案:worker_threads(Node 12+)、child_process、或拆到微服务
  • cluster 模块可以 fork 多个进程利用多核 CPU,PM2 内置了 cluster mode
CommonJS 和 ESModule 的区别?
CJSrequire/module.exports,运行时加载,同步,值拷贝。
ESMimport/export,编译时静态分析,异步,值引用(live binding)。
  • CJS 的 require 可以条件加载(if (cond) require('x'));ESM 不行,但有动态 import()
  • CJS 有缓存:require.cache,同一模块只执行一次
  • Node 中 ESM 需要 .mjs 后缀或 "type": "module";CJS 中不能直接 require ESM 模块
  • Tree-shaking 依赖 ESM 的静态结构,CJS 无法做到
Stream 是什么?有哪些类型?
Stream 是处理大数据量的核心抽象,不需要把整个数据加载到内存,而是分片处理。
  • Readablefs.createReadStreamhttp.IncomingMessage
  • Writablefs.createWriteStreamhttp.ServerResponse
  • Duplex:双向,如 net.Socket
  • Transform:转换流,如 zlib.createGzip()
  • 经典用法:readStream.pipe(transformStream).pipe(writeStream)
  • 背压(Backpressure):当下游消费速度 < 上游生产速度时,pipe 会自动暂停上游,防止内存溢出

2 常用模块与中间件

Express / Koa 中间件机制有什么区别?
Express:线性中间件,next() 传递,回调风格;错误中间件 (err, req, res, next) 四参数。
Koa:洋葱模型,async/await + next(),请求进入时从外到内,响应返回时从内到外。
// Koa 洋葱模型
app.use(async (ctx, next) => {
  console.log('1-进入');
  await next();
  console.log('1-返回');   // 响应阶段
});

app.use(async (ctx, next) => {
  console.log('2-进入');
  await next();
  console.log('2-返回');
});
// 输出: 1-进入 → 2-进入 → 2-返回 → 1-返回
  • Koa 洋葱模型天然支持前置逻辑(鉴权)+ 后置逻辑(日志、耗时统计)写在同一个中间件里
  • Express 的 next() 不返回 Promise,无法优雅地做后置处理
如何处理 Node 中的错误和未捕获异常?
  • 同步:try/catch
  • 异步回调:error-first 回调 (err, result) => {}
  • Promise.catch() 或 async/await + try/catch
  • 全局兜底process.on('uncaughtException')(同步)、process.on('unhandledRejection')(Promise)
  • 生产环境应在全局兜底中记录日志并优雅退出,用 PM2 自动重启
RESTful API 设计要点?
  • 资源用名词复数:/api/users,操作用 HTTP 方法:GET / POST / PUT / PATCH / DELETE
  • 状态码语义化:200 成功、201 创建、400 参数错误、401 未认证、403 无权限、404 不存在、500 服务端错误
  • 分页:?page=1&limit=20,响应带 totalhasMore
  • 版本控制:URL 前缀 /api/v1/ 或 Header Accept: application/vnd.api.v1+json
  • 统一响应格式:{ code, message, data }

3 数据库基础概念

数据库、表、Schema 分别是什么?
类比文件系统:数据库 = 文件夹,表/集合 = 文件,行/文档 = 文件里的一条记录,Schema = 文件的格式规范。
关系型(MySQL)              文档型(MongoDB)
──────────────────────────────────────────────
Database(数据库)           Database(数据库)
  └─ Table(表)               └─ Collection(集合)
       └─ Row(行)                 └─ Document(文档,JSON)
            └─ Column(列)              └─ Field(字段)

Schema = 表/集合的结构定义(有哪些字段、什么类型、哪些约束)
  • Database:逻辑隔离单位,一个应用通常对应一个数据库(如 helios_db
  • Table / Collection:存同类数据的容器。MySQL 的表有固定列结构;MongoDB 的集合内文档结构可以不同(但实际中通过 Mongoose Schema 约束)
  • Row / Document:一条具体的数据记录。MySQL 行的字段固定;MongoDB 文档是 JSON/BSON,字段灵活
  • Schema:定义数据的"形状"——字段名、数据类型、是否必填、默认值、唯一性约束等
MySQL Schema 和 MongoDB Schema 的区别?
-- MySQL: Schema 由 DDL 强制定义,修改需要 ALTER TABLE
CREATE TABLE users (
  id        INT PRIMARY KEY AUTO_INCREMENT,
  email     VARCHAR(255) NOT NULL UNIQUE,
  name      VARCHAR(100) NOT NULL,
  role      ENUM('user', 'admin') DEFAULT 'user',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
// MongoDB (Mongoose): Schema 在应用层定义,数据库本身不强制
const userSchema = new Schema({
  email:     { type: String, required: true, unique: true },
  name:      { type: String, required: true },
  role:      { type: String, enum: ['user', 'admin'], default: 'user' },
  createdAt: { type: Date, default: Date.now }
});
  • MySQL:Schema 是数据库层面强制的(DDL),不符合结构的数据直接报错插不进去
  • MongoDB:数据库本身 Schema-free,Mongoose 在应用层做校验。不用 Mongoose 的话什么 JSON 都能插
  • 修改成本:MySQL 改表结构要 ALTER TABLE(大表锁表风险);MongoDB 直接改 Schema 定义,旧文档不受影响(但可能有字段缺失)
  • 实际选择:需要严格数据一致性 → MySQL;需要快速迭代、数据结构多变 → MongoDB
什么是主键、外键、索引?
  • 主键(Primary Key):每条记录的唯一标识。MySQL 常用自增 INT;MongoDB 自动生成 _id(ObjectId,含时间戳+机器ID+随机数)
  • 外键(Foreign Key):关联另一张表的主键,保证引用完整性。MySQL 有 FOREIGN KEY 约束;MongoDB 没有外键,用 ObjectId 引用 + 应用层维护
  • 索引(Index):加速查询的数据结构(类比书的目录)。没索引 → 全表扫描;有索引 → B-Tree / B+Tree 快速定位
  • 索引代价:加速读但减慢写(每次插入/更新要同步维护索引),占用额外存储空间
什么是事务?ACID 是什么?
事务是一组操作的原子执行单位:"要么全成功,要么全回滚"。经典场景:转账(A 扣钱 + B 加钱必须同时成功)。
  • A - Atomicity(原子性):事务内操作不可分割,全部成功或全部回滚
  • C - Consistency(一致性):事务前后数据满足所有约束(如余额不能为负)
  • I - Isolation(隔离性):并发事务互不干扰,各隔离级别(读未提交 → 读已提交 → 可重复读 → 串行化)权衡性能和一致性
  • D - Durability(持久性):事务提交后数据持久化到磁盘,宕机也不丢失
  • MongoDB:4.0+ 支持多文档事务,但设计上应尽量通过文档嵌入避免事务;单文档写入天然原子
SQL 和 NoSQL 的核心区别?
维度          SQL(关系型)              NoSQL(非关系型)
──────────────────────────────────────────────────────────
数据模型      表 + 行 + 列              文档 / KV / 列族 / 图
Schema        固定,DDL 定义            灵活,Schema-free
查询语言      SQL                       各家 API(MongoDB Query)
事务          强 ACID                   通常 BASE,部分支持 ACID
扩展方式      垂直扩展为主              水平扩展(分片)
JOIN          原生支持                  需应用层或 $lookup
代表          MySQL / PostgreSQL        MongoDB / Redis / Cassandra
  • CAP 定理:分布式系统最多同时满足其中两个 —— Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错)
  • MySQL 倾向 CP(一致性优先);MongoDB 倾向 AP(可用性优先,最终一致)
  • 实际项目中常混用:核心交易数据用 MySQL,日志/缓存/配置用 MongoDB/Redis

4 MongoDB 核心

MongoDB vs MySQL 怎么选?
MongoDB:文档型 NoSQL,Schema 灵活,JSON-like(BSON),适合快速迭代、嵌套数据、读多写多。
MySQL:关系型,强 Schema、事务 ACID、JOIN 能力强,适合数据关系复杂、一致性要求高的场景。
  • MongoDB 适合:日志、用户画像、CMS 内容管理、实时分析、IoT 时序数据
  • MySQL 适合:电商订单、金融交易、ERP 系统
  • MongoDB 4.0+ 支持多文档事务,但性能代价大,不应作为常规手段
MongoDB 的索引类型?
  • 单字段索引db.users.createIndex({ email: 1 })
  • 复合索引{ name: 1, age: -1 },遵循最左前缀原则
  • 唯一索引{ unique: true }
  • TTL 索引:自动过期删除,适合 session、验证码 { expireAfterSeconds: 3600 }
  • 文本索引:全文搜索 { $text: { $search: "keyword" } }
  • 地理空间索引2dsphere,支持 GeoJSON 查询
  • .explain("executionStats") 查看查询是否命中索引
聚合管道(Aggregation Pipeline)怎么用?
db.orders.aggregate([
  { $match: { status: "completed" } },
  { $group: {
      _id: "$userId",
      totalAmount: { $sum: "$amount" },
      count: { $sum: 1 }
  }},
  { $sort: { totalAmount: -1 } },
  { $limit: 10 }
]);
  • 常用阶段:$match(过滤)→ $group(分组聚合)→ $sort$project(字段投影)→ $lookup(类似 JOIN)
  • $match 尽量放前面,可以利用索引减少后续阶段数据量
  • $lookup 实现关联查询,但大数据量下性能不如提前冗余

5 Schema 设计与查询

嵌入(Embedding)vs 引用(Reference)怎么选?
嵌入:把子文档直接嵌在父文档中,一次查询拿到所有数据,适合 1:1 或 1:少量。
引用:只存 ObjectId,需要额外查询或 $lookup,适合 1:N 大量或 M:N。
  • 嵌入优点:读性能好(无 JOIN)、原子写入。缺点:文档体积膨胀(上限 16MB)、子文档更新需整文档写入
  • 引用优点:数据独立、无体积问题。缺点:需多次查询、没有外键约束
  • 实战经验:评论列表(数量不可控)用引用;用户的收货地址(少量固定)用嵌入
Mongoose 的核心概念?
  • Schema:定义文档结构和验证规则
  • Model:由 Schema 编译而来,对应一个 Collection,提供 CRUD 方法
  • Document:Model 的实例,对应一条记录
  • 中间件(Hooks)pre('save') / post('save'),可做密码加密、日志等
  • Virtual:虚拟字段,不存 DB,如 fullName = firstName + lastName
  • Populate:自动填充引用字段 .populate('author')
const userSchema = new Schema({
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true, select: false },
  role: { type: String, enum: ['user', 'admin'], default: 'user' },
  createdAt: { type: Date, default: Date.now }
});

userSchema.pre('save', async function(next) {
  if (this.isModified('password')) {
    this.password = await bcrypt.hash(this.password, 12);
  }
  next();
});

6 全栈联调实战

前后端联调的常见痛点和解决方案?
  • 接口约定不一致:用 Swagger / OpenAPI 定义接口文档,前端据此 mock,后端据此实现,CI 校验一致性
  • 跨域问题:开发环境用 Vite/Webpack devServer proxy;生产用 Nginx 反向代理或 CORS 配置
  • 数据格式差异:后端返回 snake_case,前端用 camelCase → 写 axios 拦截器做统一转换
  • 认证联调:JWT 存 HttpOnly Cookie,axios 设 withCredentials: true,后端配合 CORS credentials: true
  • 错误排查:约定统一错误码体系 + request-id 透传,前端 console 能定位到后端哪条日志
你做过哪些全栈相关的实践?
结合简历:了解 Node.js、MongoDB,具备一定全栈开发与联调能力。面试时可举的例子:
  • 用 Express/Koa 搭建 BFF 层做数据聚合,前端一次请求拿到多个微服务的数据,减少客户端请求数
  • 用 MongoDB 存储非结构化数据(用户操作日志、配置项),Mongoose 做 Schema 校验和数据迁移
  • 写过 Node CLI 工具自动化构建流程(脚手架、代码生成、批量文件处理)
  • 联调时用 Charles/Whistle 做请求代理和数据 mock,定位前后端数据不一致问题