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。
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.thensetImmediate在 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 的区别?
CJS:
ESM:
require/module.exports,运行时加载,同步,值拷贝。ESM:
import/export,编译时静态分析,异步,值引用(live binding)。
- CJS 的
require可以条件加载(if (cond) require('x'));ESM 不行,但有动态import() - CJS 有缓存:
require.cache,同一模块只执行一次 - Node 中 ESM 需要
.mjs后缀或"type": "module";CJS 中不能直接requireESM 模块 - Tree-shaking 依赖 ESM 的静态结构,CJS 无法做到
Stream 是什么?有哪些类型?
Stream 是处理大数据量的核心抽象,不需要把整个数据加载到内存,而是分片处理。
- Readable:
fs.createReadStream、http.IncomingMessage - Writable:
fs.createWriteStream、http.ServerResponse - Duplex:双向,如
net.Socket - Transform:转换流,如
zlib.createGzip() - 经典用法:
readStream.pipe(transformStream).pipe(writeStream) - 背压(Backpressure):当下游消费速度 < 上游生产速度时,
pipe会自动暂停上游,防止内存溢出
2 常用模块与中间件
Express / Koa 中间件机制有什么区别?
Express:线性中间件,
Koa:洋葱模型,
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,响应带total、hasMore - 版本控制:URL 前缀
/api/v1/或 HeaderAccept: 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 能力强,适合数据关系复杂、一致性要求高的场景。
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,需要额外查询或
引用:只存 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,后端配合 CORScredentials: true - 错误排查:约定统一错误码体系 + request-id 透传,前端 console 能定位到后端哪条日志
你做过哪些全栈相关的实践?
结合简历:了解 Node.js、MongoDB,具备一定全栈开发与联调能力。面试时可举的例子:
- 用 Express/Koa 搭建 BFF 层做数据聚合,前端一次请求拿到多个微服务的数据,减少客户端请求数
- 用 MongoDB 存储非结构化数据(用户操作日志、配置项),Mongoose 做 Schema 校验和数据迁移
- 写过 Node CLI 工具自动化构建流程(脚手架、代码生成、批量文件处理)
- 联调时用 Charles/Whistle 做请求代理和数据 mock,定位前后端数据不一致问题