好的,这是一份涵盖 Node.js、MySQL、Redis、MongoDB 以及数据库设计和操作的高频面试题列表,采用一问一答和 Markdown 格式。
Node.js高频面试题汇总
Node.js
Q: 请解释一下 Node.js 的事件循环(Event Loop)机制。
A: Node.js 事件循环是其非阻塞 I/O 模型的核心。它允许 Node.js 在单个线程中执行 I/O 操作,而不会阻塞主线程。
- 执行栈 (Call Stack): 同步代码在此执行。
- Node APIs / Web APIs: 异步操作(如
setTimeout, 文件读写, 网络请求)被交给底层 C++ APIs 或 libuv 处理。 - 回调队列 (Callback Queue / Task Queue): 当异步操作完成时,其回调函数会被放入此队列。常见的有 Timer 队列、I/O 队列、Check 队列等。
- 事件循环 (Event Loop): 持续检查执行栈是否为空。如果为空,它会从回调队列中取出一个回调函数,压入执行栈执行。 这个过程不断重复,使得 Node.js 能够高效地处理大量并发连接。Node.js 11 之后引入了
worker_threads来处理 CPU 密集型任务,但核心的 I/O 仍然依赖事件循环。
Q: Node.js 中的 process.nextTick() 和 setImmediate() 有什么区别?
A: 它们都用于将函数推迟到下一个事件循环迭代执行,但执行时机不同:
process.nextTick(): 其回调函数会在 当前 事件循环阶段完成后、下一个阶段开始前 立即执行。它属于 "microtask" 队列,优先级高于 "macrotask" 队列(如setTimeout,setImmediate)。setImmediate(): 其回调函数会被放入事件循环的 Check 阶段 执行。它会在 I/O 事件回调之后、setTimeout(fn, 0)之前或之后执行(取决于系统调度和当前循环状态)。
通常,process.nextTick() 的优先级更高,会更快执行。
Q: 解释一下 Node.js 中的回调地狱(Callback Hell)以及如何解决它?
A: 回调地狱 指的是在 Node.js 中处理多个嵌套的异步操作时,回调函数层层嵌套,导致代码难以阅读、理解和维护,形成金字塔形状。 解决方法:
- 模块化: 将复杂的逻辑拆分成更小的、可重用的函数。
- 命名函数: 使用具名函数代替匿名函数,提高可读性。
- Promises: 使用 Promise 对象来管理异步操作。
.then()用于处理成功结果,.catch()用于处理错误,可以链式调用,避免深层嵌套。 - Async/Await (基于 Promises): 这是目前最推荐的方式。使用
async关键字声明异步函数,使用await关键字等待 Promise 解析,使得异步代码看起来像同步代码,极大提高了可读性和可维护性。
Q: CommonJS 和 ES Modules 在 Node.js 中有什么主要区别?
A:
- 加载方式:
- CommonJS (
require/module.exports): 同步加载模块。代码在运行时加载和解析。适合服务器端。 - ES Modules (
import/export): 异步加载模块(尽管在 Node.js 当前实现中顶层await出现前,表现上更接近同步)。代码在解析阶段(编译时)确定依赖关系。
- CommonJS (
- 值的类型:
- CommonJS: 导出的是值的 拷贝。如果是基本类型,导入后修改不会影响原模块;如果是对象,则共享引用。
- ES Modules: 导出的是值的 动态只读引用 (live binding)。导入的变量不能重新赋值,但如果导出的是对象,可以修改对象的属性。
- 语法: 语法不同 (
requirevsimport,module.exports/exportsvsexport/export default)。 this指向: CommonJS 模块顶层的this指向module.exports(或exports);ES Modules 模块顶层的this是undefined。- 兼容性: Node.js 早期只支持 CommonJS,现在通过
.mjs文件扩展名或在package.json中设置"type": "module"来支持 ES Modules。
Q: Node.js 中的 Stream(流)是什么?有什么好处?
A: Stream 是一种在 Node.js 中处理流式数据的抽象接口。数据不是一次性加载到内存中,而是分块(chunk)处理。 类型:
- Readable Streams (可读流): 如
fs.createReadStream() - Writable Streams (可写流): 如
fs.createWriteStream(),http.ClientRequest - Duplex Streams (双工流): 可读也可写,如
net.Socket - Transform Streams (转换流): 在读写过程中修改或转换数据的双工流,如
zlib.createGzip()
好处:
- 内存效率: 不需要将大文件或大数据完全加载到内存中,显著降低内存消耗。
- 时间效率: 数据可用时即可开始处理,不必等待所有数据都传输完毕。
- 可组合性: 可以通过
pipe()方法将多个流连接起来,形成处理管道,代码简洁优雅。例如:readable.pipe(transform).pipe(writable)。
MySQL
Q: 解释一下 ACID 是什么?InnoDB 存储引擎是如何保证 ACID 的?
A: ACID 是数据库事务正确执行的四个基本要素:
- 原子性 (Atomicity): 事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。如果事务中任何一部分失败,整个事务将回滚到初始状态。
- 一致性 (Consistency): 事务必须使数据库从一个一致性状态转换到另一个一致性状态。事务开始前和结束后,数据库的完整性约束没有被破坏。
- 隔离性 (Isolation): 一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。这通过不同的隔离级别实现。
- 持久性 (Durability): 一旦事务提交,则其所做的修改就会永久保存到数据库中。即使系统崩溃,修改的数据也不会丢失。
InnoDB 如何保证 ACID:
- 原子性: 通过
undo log(回滚日志) 实现。事务执行过程中,修改前的数据会记录在undo log中,如果事务失败或需要回滚,可以根据undo log恢复到事务开始前的状态。 - 一致性: 由原子性、隔离性、持久性共同保证,同时还依赖于数据库自身的约束(如主键、外键、唯一键、数据类型检查等)。
- 隔离性: 通过锁机制(行锁、表锁、间隙锁)和 MVCC (多版本并发控制) 实现。MVCC 允许读操作不阻塞写操作,写操作也不阻塞读操作,提高了并发性能。不同的隔离级别对应不同的锁策略和 MVCC 可见性规则。
- 持久性: 通过
redo log(重做日志) 实现。事务提交前,修改操作会先写入redo log buffer,并根据策略刷盘 (innodb_flush_log_at_trx_commit参数)。即使数据库崩溃,重启后也可以通过redo log恢复已提交事务的数据。
Q: MySQL 中有哪些常见的索引类型?它们的应用场景是什么?
A:
- B-Tree 索引 (默认):
- 原理: 基于 B+ 树数据结构,适用于范围查询和精确查找。支持
>=、<=、BETWEEN、IN以及LIKE前缀匹配 ('abc%')。 - 应用: 最常用的索引类型,适用于大多数查询场景。主键索引、唯一索引、普通索引通常都是 B-Tree 索引。
- 原理: 基于 B+ 树数据结构,适用于范围查询和精确查找。支持
- 哈希索引 (Hash Index):
- 原理: 基于哈希表实现,只适用于精确匹配 (
=、IN) 查询,速度非常快。不支持范围查询和排序。 - 应用: 主要由 Memory 存储引擎支持。InnoDB 支持 "自适应哈希索引",是内部优化,用户不能手动创建。适合等值查询频繁的场景。
- 原理: 基于哈希表实现,只适用于精确匹配 (
- 全文索引 (Full-Text Index):
- 原理: 用于在文本数据(
CHAR,VARCHAR,TEXT)中进行关键词搜索。使用倒排索引等技术。 - 应用: 适合搜索引擎类的文本匹配查询 (
MATCH() AGAINST())。MyISAM 和 InnoDB (5.6+) 都支持。
- 原理: 用于在文本数据(
- 空间索引 (Spatial Index / R-Tree):
- 原理: 用于地理空间数据类型(如
GEOMETRY,POINT)的索引,基于 R 树。 - 应用: 地理信息系统 (GIS),用于查找附近的位置等空间查询。
- 原理: 用于地理空间数据类型(如
按逻辑分类:
- 主键索引 (Primary Key): 唯一标识一行,不允许为空,一个表只能有一个。InnoDB 中是聚簇索引。
- 唯一索引 (Unique Index): 索引列的值必须唯一,但允许有空值 (NULL)。
- 普通索引 (Normal/Non-Unique Index): 最基本的索引,没有任何限制。
- 组合索引 (Composite Index): 在多个列上创建的索引。遵循“最左前缀原则”。
Q: 什么是聚簇索引和非聚簇索引?
A:
- 聚簇索引 (Clustered Index):
- 数据存储的物理顺序与索引顺序一致。叶子节点直接存储整行数据。
- 在 InnoDB 中,主键索引就是聚簇索引。如果没有定义主键,InnoDB 会选择一个唯一的非空索引代替;如果没有这样的索引,InnoDB 会隐式定义一个 6 字节的
ROWID作为聚簇索引。 - 优点: 基于主键的查询和范围查询速度快。
- 缺点: 插入和更新可能导致页分裂,开销较大;二级索引查询需要回表。一个表只能有一个聚簇索引。
- 非聚簇索引 (Non-Clustered Index) / 二级索引 (Secondary Index):
- 数据存储的物理顺序与索引顺序无关。叶子节点存储的是索引键值和指向数据行的指针(在 InnoDB 中是指向主键的值)。
- 优点: 插入和更新相对聚簇索引开销较小。
- 缺点: 如果查询需要获取非索引列的数据,需要进行回表操作(根据叶子节点存储的主键值再去聚簇索引中查找完整的行数据),增加 I/O 次数。
- 覆盖索引 (Covering Index): 如果查询所需的所有列都包含在二级索引中,就不需要回表,称为覆盖索引,可以提高查询性能。
Q: 解释一下 MySQL 的事务隔离级别。
A: SQL 标准定义了四种事务隔离级别,用于控制事务并发执行时可能出现的问题(脏读、不可重复读、幻读):
- 读未提交 (Read Uncommitted):
- 最低级别。一个事务可以读取到另一个未提交事务的修改。
- 问题: 可能导致脏读 (Dirty Read)、不可重复读 (Non-Repeatable Read)、幻读 (Phantom Read)。
- 应用: 很少使用。
- 读已提交 (Read Committed):
- 一个事务只能读取到另一个已经提交的事务的修改。解决了脏读。
- 问题: 可能导致不可重复读、幻读。这是大多数数据库(如 Oracle, SQL Server)的默认隔离级别。
- 实现 (InnoDB): 通过 MVCC 实现,每次
SELECT都会创建新的 Read View。
- 可重复读 (Repeatable Read):
- 保证在同一个事务中多次读取同一数据的结果是一致的。解决了脏读和不可重复读。
- 问题: 可能导致幻读。这是 MySQL InnoDB 的默认隔离级别。
- 实现 (InnoDB): 通过 MVCC 实现,事务开始时创建 Read View,并在整个事务期间使用该 Read View。通过间隙锁 (Gap Lock) 和 Next-Key Lock 来防止幻读。
- 可串行化 (Serializable):
- 最高级别。强制事务串行执行,避免了所有并发问题(脏读、不可重复读、幻读)。
- 问题: 并发性能最低。
- 实现 (InnoDB): 通常会对所有读取的行加共享锁 (S Lock),写入时加排他锁 (X Lock),或者直接将所有
SELECT语句隐式转为SELECT ... LOCK IN SHARE MODE。
Q: 如何优化 MySQL 查询?(Explain 命令的作用)
A: 查询优化方法:
- 索引优化:
- 为
WHERE、JOIN、ORDER BY、GROUP BY子句中频繁使用的列创建合适的索引。 - 利用覆盖索引避免回表。
- 注意组合索引的最左前缀原则。
- 避免在索引列上使用函数或进行计算。
- 定期分析和优化索引 (
ANALYZE TABLE)。
- 为
- SQL 语句优化:
- 避免
SELECT *,只选择需要的列。 - 使用
JOIN代替子查询(视情况而定,有时子查询更优)。 - 优化
LIMIT分页查询(例如,使用延迟关联或记录上次查询的最大 ID)。 - 避免在
WHERE子句中使用!=或<>操作符,可能导致索引失效。 - 使用
UNION ALL代替UNION(如果不需要去重)。
- 避免
- 数据库结构优化:
- 选择合适的数据类型。
- 遵循范式设计,但也考虑反范式设计以提高查询性能(冗余字段)。
- 考虑分区表 (
PARTITIONING) 处理大数据表。
- 服务器配置优化:
- 调整 MySQL 配置参数(如
innodb_buffer_pool_size,query_cache_size(已废弃),max_connections等)。
- 调整 MySQL 配置参数(如
- 使用查询分析工具:
- 使用
EXPLAIN命令分析查询执行计划。 - 使用慢查询日志 (
slow_query_log) 找出性能瓶颈。 - 使用
SHOW PROFILE或Performance Schema进行更详细的分析。
- 使用
EXPLAIN 命令的作用:EXPLAIN 用于显示 MySQL 如何执行一条 SELECT 语句。它会输出关于查询执行计划的信息,帮助我们理解查询的瓶颈所在。关键输出列包括:
id: 查询的标识符。select_type: 查询类型(SIMPLE, PRIMARY, SUBQUERY, DERIVED, UNION 等)。table: 涉及的表。partitions: 匹配的分区。type: 访问类型,表示 MySQL 如何查找表中的行。从最好到最差:system>const>eq_ref>ref>range>index>ALL。ALL表示全表扫描,通常需要优化。possible_keys: 可能使用的索引。key: 实际使用的索引。如果为 NULL,表示没有使用索引。key_len: 使用的索引的长度。长度越短越好。ref: 显示索引的哪一列被使用了。rows: MySQL 估计为了找到所需的行而要读取的行数。filtered: 按表条件过滤的行百分比的估计值。Extra: 包含不适合在其他列中显示但十分重要的额外信息,如Using index(使用了覆盖索引),Using where(使用了 WHERE 过滤),Using temporary(使用了临时表),Using filesort(使用了文件排序,性能较差)。
通过分析 EXPLAIN 的输出,特别是 type, key, rows, Extra 列,可以判断查询是否高效,是否命中了合适的索引,从而进行针对性的优化。
Redis
Q: Redis 是什么?它有哪些常见的应用场景?
A: Redis (Remote Dictionary Server) 是一个开源的、使用 C 语言编写的、基于内存运行并支持持久化的、高性能的 Key-Value 数据库。它支持多种数据结构,如 Strings, Hashes, Lists, Sets, Sorted Sets 等。 特点:
- 高性能: 基于内存操作,读写速度非常快 (通常在微秒级别)。
- 丰富的数据结构: 支持多种复杂数据结构,能满足不同场景需求。
- 持久化: 支持 RDB (快照) 和 AOF (追加日志) 两种持久化方式,保证数据在服务重启后不丢失。
- 原子操作: Redis 的大部分命令都是原子性的,可以用来实现锁等功能。
- 支持发布/订阅、事务、Lua 脚本等。
- 高可用与集群: 支持 Sentinel (哨兵) 实现高可用,支持 Cluster 实现分布式存储。
常见应用场景:
- 缓存 (Cache): 最常见的应用。将热点数据(如数据库查询结果、页面片段)缓存在 Redis 中,减轻后端数据库压力,提高访问速度。
- 会话存储 (Session Store): 将用户的 Session 信息存储在 Redis 中,实现分布式系统中的会话共享。
- 计数器/限流器 (Counter/Rate Limiter): 利用 Redis 原子性的
INCR/DECR命令实现计数器;结合过期时间实现限流功能。 - 排行榜/积分榜 (Leaderboard): 利用 Sorted Set (有序集合) 数据结构,可以方便地实现带分数的排序列表。
- 发布/订阅 (Pub/Sub): 用于构建消息队列、实时通知系统等。
- 分布式锁 (Distributed Lock): 利用
SETNX(SET if Not eXists) 或带有NXEX选项的SET命令实现分布式环境下的锁机制。 - 地理位置服务 (Geo): 利用 Redis 的 GEO 数据结构 (基于 Sorted Set) 实现存储地理位置信息、计算距离、查找附近地点等功能。
- 队列 (Queue): 利用 List 数据结构的
LPUSH/RPOP(或RPUSH/LPOP) 实现简单的消息队列;或使用更专业的 Streams 数据类型。
Q: Redis 的持久化机制有哪些?它们有什么区别?
A: Redis 提供两种主要的持久化方式:RDB 和 AOF。
- RDB (Redis DataBase) - 快照:
- 原理: 在指定的时间间隔内,将内存中的数据集快照写入磁盘上的一个二进制文件 (
dump.rdb)。可以通过SAVE(阻塞) 或BGSAVE(后台非阻塞) 命令触发,也可以配置自动触发。 - 优点:
- 生成的文件紧凑,适合备份和灾难恢复。
- 恢复速度比 AOF 快,因为直接加载二进制文件。
- 对 Redis 性能影响较小 (使用
BGSAVE时,父进程只需 fork 一个子进程)。
- 缺点:
- 如果 Redis 意外宕机,会丢失最后一次快照之后的所有数据修改。实时性较差。
fork()操作在数据集较大时可能会消耗较多 CPU 和内存,导致短暂的服务停顿。
- 原理: 在指定的时间间隔内,将内存中的数据集快照写入磁盘上的一个二进制文件 (
- AOF (Append Only File) - 追加日志:
- 原理: 将 Redis 执行过的所有写命令追加到文件末尾 (
appendonly.aof)。Redis 重启时会重新执行 AOF 文件中的命令来恢复数据。 - 优点:
- 数据安全性更高。根据配置 (
appendfsync策略:always,everysec,no),可以做到丢失更少的数据(最多丢失 1 秒的数据)。 - AOF 文件是可读的文本格式(Redis 7 之前),易于理解和修复。
- 数据安全性更高。根据配置 (
- 缺点:
- 相同数据集,AOF 文件通常比 RDB 文件大。
- 恢复速度通常比 RDB 慢,因为需要逐条执行命令。
- 根据
appendfsync策略,写入性能可能低于 RDB (尤其是always策略)。
- 原理: 将 Redis 执行过的所有写命令追加到文件末尾 (
区别总结:
| 特性 | RDB (快照) | AOF (追加日志) |
|---|---|---|
| 原理 | 保存某个时间点的数据快照 | 记录所有写操作命令 |
| 数据安全 | 可能丢失最后一次快照后的数据 | 更高,根据 fsync 策略可丢失很少数据 |
| 文件大小 | 紧凑的二进制文件 | 通常更大,文本或混合格式 |
| 恢复速度 | 快 | 相对较慢 |
| 性能影响 | BGSAVE 时 fork() 可能有影响 | 写操作可能影响性能 (取决于 fsync) |
| 触发方式 | 手动 (SAVE/BGSAVE)/自动配置 | 自动开启,写命令即记录 |
选择与混合使用:
- 如果能容忍少量数据丢失,追求快速恢复和备份,可以选择 RDB。
- 如果需要更高的数据安全性,可以选择 AOF。
- 推荐: 同时开启 RDB 和 AOF。Redis 重启时会优先使用 AOF 文件来恢复数据(因为它通常更完整),同时 RDB 可用于备份。AOF 文件过大时,Redis 会进行 AOF 重写 (rewrite),生成一个更小的、包含当前数据状态的最少命令集的新 AOF 文件。
Q: Redis 的数据类型有哪些?请简述其应用场景。
A: Redis 支持多种丰富的数据类型:
- String (字符串):
- 描述: 最基本的数据类型,二进制安全,最大可以存储 512MB。
- 应用: 缓存(页面、对象序列化)、计数器 (
INCR/DECR)、分布式锁 (SETNX)、存储简单的键值对。
- Hash (哈希/字典):
- 描述: 一个 String 类型的 field 和 value 的映射表,适合存储对象。
- 应用: 存储对象信息(如用户信息,包含多个字段),比将对象序列化成 JSON 存入 String 更节省空间(尤其是字段较少时)且方便修改单个字段。
- List (列表):
- 描述: 简单的字符串列表,按照插入顺序排序。可以在列表头部 (left) 或尾部 (right) 添加/删除元素。底层是双向链表或 ziplist/quicklist。
- 应用: 消息队列(
LPUSH/RPOP)、栈(LPUSH/LPOP)、最新消息列表(LPUSH/LTRIM)。
- Set (集合):
- 描述: String 类型的无序集合。元素唯一,不允许重复。支持集合间操作(交集、并集、差集)。
- 应用: 标签系统(给用户打标签)、共同好友/关注、抽奖系统(随机取元素
SRANDMEMBER)。
- Sorted Set (有序集合 / ZSet):
- 描述: Set 的升级版,每个元素都会关联一个 double 类型的 score (分数)。元素根据 score 进行排序,元素自身必须唯一。
- 应用: 排行榜(根据分数排序)、带权重的队列、范围查找(按分数区间查找成员)。
- Bitmap (位图):
- 描述: 本质是 String 类型,但可以对位的级别进行操作 (
SETBIT,GETBIT,BITCOUNT,BITOP)。 - 应用: 用户签到、在线状态统计、布隆过滤器(需要自己实现)。非常节省空间。
- 描述: 本质是 String 类型,但可以对位的级别进行操作 (
- HyperLogLog:
- 描述: 用极小的内存(约 12KB)来估计一个集合的基数(不重复元素的数量),存在一定的误差。
- 应用: 统计网站的 UV(Unique Visitor)、大量数据的去重计数。
- GEO (地理空间):
- 描述: 存储地理位置信息(经度、纬度、成员名),并支持基于位置的查询(如查找附近的人/地点、计算距离)。底层基于 Sorted Set 实现。
- 应用: LBS 应用(附近的人、附近的店铺)。
- Stream (流):
- 描述: Redis 5.0 引入的类似日志的数据结构,支持消费组 (Consumer Group),提供更强大的消息队列功能。
- 应用: 消息队列、事件溯源、实时数据处理。
Q: 什么是缓存穿透、缓存击穿和缓存雪崩?如何解决?
A: 这三个是使用缓存时常见的问题:
- 缓存穿透 (Cache Penetration):
- 描述: 查询一个数据库和缓存中都不存在的数据。导致每次请求都会直接打到数据库,失去了缓存的意义,当流量大时可能压垮数据库。通常是恶意攻击或代码逻辑错误导致查询了非法 key。
- 解决:
- 接口层校验: 对请求参数进行校验,过滤非法参数。
- 缓存空值 (Cache Null): 如果查询数据库发现数据不存在,仍然将一个**空值(或特殊约定值)**缓存起来,并设置较短的过期时间。后续相同请求会命中空值缓存,不会再访问数据库。
- 布隆过滤器 (Bloom Filter): 在访问缓存前,使用布隆过滤器判断 key 是否可能存在。如果过滤器认为 key 不存在,则直接返回,避免查询缓存和数据库。布隆过滤器有误判率(认为存在的可能实际不存在),但不会漏判(认为不存在的一定不存在)。
- 缓存击穿 (Cache Breakdown):
- 描述: 一个热点 Key 在缓存中过期失效的瞬间,同时有大量的并发请求访问这个 Key。这些请求都会穿透缓存直接打到数据库,导致数据库压力瞬时增大。
- 解决:
- 设置热点数据永不过期: 对于极热点的数据,可以不设置过期时间,或者通过后台任务定期更新缓存。
- 加互斥锁 (Mutex Lock): 当缓存失效时,只允许第一个请求去查询数据库并重建缓存,其他请求等待。可以使用 Redis 的
SETNX或其他分布式锁实现。获取锁的线程负责加载数据到缓存,然后释放锁。其他线程在获取锁失败后,可以稍等片刻再重试获取缓存。
- 缓存雪崩 (Cache Avalanche):
- 描述: 大量的缓存 Key 在同一时间集中过期失效,或者 Redis 服务自身宕机。导致大量的请求无法命中缓存,直接涌向数据库,造成数据库压力剧增甚至崩溃。
- 解决:
- 针对 Key 集中过期:
- 随机化过期时间: 在基础过期时间上增加一个随机值,避免大量 Key 同时失效。例如,过期时间设置为
T + random(0, T*0.1)。 - 设置热点数据永不过期: 同缓存击穿的解决方法。
- 随机化过期时间: 在基础过期时间上增加一个随机值,避免大量 Key 同时失效。例如,过期时间设置为
- 针对 Redis 宕机:
- 服务熔断/降级: 当检测到 Redis 不可用时,暂时停止访问 Redis,或者返回默认值/静态数据,或者限制访问流量,防止所有请求打到数据库。
- 高可用集群: 部署 Redis Sentinel 或 Redis Cluster,保证 Redis 服务的高可用性。当主节点宕机时,可以自动切换到从节点。
- 多级缓存: 使用本地缓存 (如 Guava Cache, Caffeine) + 分布式缓存 (Redis) 的架构。
- 针对 Key 集中过期:
MongoDB
Q: MongoDB 是什么类型的数据库?它和关系型数据库 (如 MySQL) 的主要区别是什么?
A: MongoDB 是一种文档型 (Document-Oriented) 的 NoSQL (Not Only SQL) 数据库。它存储的是 BSON (Binary JSON) 格式的文档,结构灵活,类似于 JSON 对象。 主要区别:
| 特性 | MongoDB (文档型 NoSQL) | MySQL (关系型 SQL) |
|---|---|---|
| 数据模型 | 文档 (BSON),集合 (Collection) | 表 (Table),行 (Row),列 (Column) |
| 数据结构 | 模式灵活 (Schema-Free/Flexible) | 模式固定 (Fixed Schema) |
| 数据关系 | 通过内嵌文档或引用表达关系 | 通过外键和连接 (JOIN) 表达关系 |
| 查询语言 | MongoDB Query Language (MQL), 类 JSON | SQL (Structured Query Language) |
| 事务支持 | 支持多文档事务 (v4.0+),单文档原子性 | 完全支持 ACID 事务 |
| 扩展性 | 水平扩展能力强 (通过 Sharding) | 垂直扩展为主,水平扩展相对复杂 (分库分表) |
| 一致性 | 最终一致性 (默认),可配置 | 强一致性 (ACID) |
| 适用场景 | 大数据、非结构化/半结构化数据、快速迭代 | 结构化数据、事务要求高、复杂关系查询 |
总结:
- MySQL 适用于需要严格数据一致性、事务支持、复杂关系查询的场景,数据结构相对固定。
- MongoDB 适用于数据结构不固定或经常变化、需要高写入性能和水平扩展能力的场景,对事务要求相对较低(虽然现在支持多文档事务,但使用场景和复杂度与 RDBMS 不同)。
Q: 解释一下 MongoDB 中的 BSON 是什么?
A: BSON (Binary JSON) 是一种二进制的数据序列化格式,用于在 MongoDB 中存储文档和进行网络数据传输。它扩展了 JSON 的表示能力,增加了额外的数据类型。 特点与优点:
- 二进制格式: 相比 JSON (文本格式),BSON 更紧凑,解析速度更快。
- 更多数据类型: 支持 JSON 没有的数据类型,如
Date,Binary data,ObjectID,Regular Expression,JavaScript Code,Timestamp,Int32,Int64,Decimal128等。这使得 MongoDB 可以更精确地表示和处理各种数据。 - 轻量级: 设计目标是轻量、可遍历、高效。
- 类型和长度信息前缀: BSON 文档和元素都带有类型和长度信息,使得解析器可以轻松地跳过不需要的字段,提高了查询效率。
MongoDB 使用 BSON 作为其内部数据存储格式,所有存入 MongoDB 的文档都会被转换成 BSON 格式。
Q: MongoDB 中的索引有哪些类型?如何创建索引?
A: MongoDB 支持多种索引类型来提高查询性能:
- 单字段索引 (Single Field Index): 最基本的索引,在单个字段上创建。
- 复合索引 (Compound Index): 在多个字段上创建的索引。索引的顺序非常重要,影响查询效率。MongoDB 可以利用复合索引的前缀来满足查询。
- 多键索引 (Multikey Index): 针对数组字段创建的索引。MongoDB 会为数组中的每个元素创建索引条目。
- 地理空间索引 (Geospatial Index):
2dsphere: 支持球面几何查询,用于存储 GeoJSON 对象或传统坐标对。适用于地球表面的地理位置查询。2d: 支持平面几何查询,用于存储传统坐标对。适用于二维平面的坐标查询(如游戏地图)。
- 文本索引 (Text Index): 支持在字符串内容的集合上执行文本搜索操作。一个集合最多只能有一个文本索引(但可以包含多个字段)。
- 哈希索引 (Hashed Index): 基于字段值的哈希值进行索引。只支持精确匹配,不支持范围查询。主要用于分片键 (Shard Key),有助于数据在分片集群中均匀分布。
- 唯一索引 (Unique Index): 确保索引字段的值是唯一的。如果尝试插入或更新导致重复值的文档,操作会失败。复合唯一索引确保字段组合的唯一性。
- TTL 索引 (Time-To-Live Index): 特殊的单字段索引,可以让 MongoDB 在指定的时间后自动删除文档。必须建立在日期类型字段或包含日期类型的数组上。
- 部分索引 (Partial Index): 只对集合中满足指定过滤表达式 (
partialFilterExpression) 的文档创建索引。可以节省存储空间并提高性能,适用于只索引集合子集的场景。
创建索引: 使用 db.collection.createIndex() 方法。
- 创建单字段升序索引:
db.collection.createIndex({ fieldName: 1 })(1 表示升序, -1 表示降序) - 创建复合索引:
db.collection.createIndex({ field1: 1, field2: -1 }) - 创建唯一索引:
db.collection.createIndex({ fieldName: 1 }, { unique: true }) - 创建文本索引:
db.collection.createIndex({ content: "text", title: "text" }) - 创建 TTL 索引 (60秒后过期):
db.collection.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }) - 创建部分索引 (只索引 rating > 5 的文档):
db.collection.createIndex({ rating: 1 }, { partialFilterExpression: { rating: { $gt: 5 } } })
查看索引: db.collection.getIndexes()删除索引: db.collection.dropIndex("indexName") 或 db.collection.dropIndex({ fieldName: 1 })
Q: MongoDB 的聚合管道 (Aggregation Pipeline) 是什么?请举例说明。
A: 聚合管道 是 MongoDB 中进行数据聚合和处理的框架。它受到 Unix 管道 (|) 概念的启发,将文档在一个管道 (Pipeline) 中进行一系列的阶段 (Stage) 处理。每个阶段接收前一阶段输出的文档作为输入,并对这些文档进行转换或过滤,最终输出结果。 常用阶段 (Stages):
$match: 过滤文档,类似于find()的查询条件。通常放在管道早期以减少处理的数据量。$project: 重塑文档结构,选择、重命名、添加或删除字段。$group: 按指定的键(_id)对文档进行分组,并对每个分组应用累加器表达式(如$sum,$avg,$min,$max,$push,$addToSet)。$sort: 按指定字段对文档进行排序。$limit: 限制输出的文档数量。$skip: 跳过指定数量的文档。$unwind: 将数组字段中的每个元素拆分成独立的文档输出。$lookup: 执行类似 SQL 的左外连接 (Left Outer Join),从另一个集合中查找匹配的文档并合并。$out/$merge: 将聚合结果写入新的集合或合并到现有集合。
举例: 假设有一个 orders 集合,包含订单信息:
{ "_id": 1, "cust_id": "A123", "amount": 500, "status": "A", "date": ISODate("2023-01-15") }
{ "_id": 2, "cust_id": "B456", "amount": 200, "status": "A", "date": ISODate("2023-01-16") }
{ "_id": 3, "cust_id": "A123", "amount": 300, "status": "D", "date": ISODate("2023-01-17") }
{ "_id": 4, "cust_id": "C789", "amount": 150, "status": "A", "date": ISODate("2023-01-18") }
{ "_id": 5, "cust_id": "B456", "amount": 75, "status": "A", "date": ISODate("2023-01-19") }需求:计算每个客户状态为 'A' 的订单总金额,并按总金额降序排序。
db.orders.aggregate([
// 阶段 1: 过滤状态为 'A' 的订单
{
$match: { status: "A" }
},
// 阶段 2: 按客户 ID 分组,计算每个客户的总金额
{
$group: {
_id: "$cust_id", // 按 cust_id 分组,分组键成为结果文档的 _id
totalAmount: { $sum: "$amount" } // 计算每个分组的 amount 总和,命名为 totalAmount
}
},
// 阶段 3: 按总金额降序排序
{
$sort: { totalAmount: -1 }
}
])输出可能如下:
[
{ "_id": "A123", "totalAmount": 500 },
{ "_id": "B456", "totalAmount": 275 },
{ "_id": "C789", "totalAmount": 150 }
]Q: 解释一下 MongoDB 的 Sharding (分片) 机制。
A: Sharding 是 MongoDB 水平扩展数据库能力的一种方式。它将大型数据集分割成较小的、更易于管理的部分(称为 Chunks),并将这些 Chunks 分布到多个分片 (Shard) 上。每个 Shard 是一个独立的 MongoDB 实例(通常是一个副本集 Replica Set,以保证高可用)。 核心组件:
- Shards: 存储数据的节点。每个 Shard 存储整个数据集的一个子集。生产环境中,每个 Shard 通常是一个副本集。
- Mongos (Query Routers): 查询路由器。客户端应用程序连接到
mongos,而不是直接连接到 Shards。mongos负责将客户端的请求路由到正确的 Shard(s),并将结果聚合后返回给客户端。对客户端来说,分片集群看起来像一个单一的 MongoDB 实例。可以运行多个mongos实例以实现负载均衡和高可用。 - Config Servers: 配置服务器。存储集群的元数据 (Metadata),包括哪些数据在哪个 Shard 上(Chunk 的分布信息)以及集群的配置信息。配置服务器自身也需要高可用,通常部署为副本集 (CSRS - Config Server Replica Set)。
工作原理:
- 分片键 (Shard Key): 选择集合中的一个或多个字段作为分片键。MongoDB 根据分片键的值将集合中的文档划分到不同的 Chunks。选择好的分片键对集群性能和数据均衡至关重要。
- Chunk 分割: 当一个 Chunk 的大小超过配置的阈值时,MongoDB 会自动将其分割成两个较小的 Chunks。
- 数据均衡 (Balancer): 后台进程 Balancer 会监控各 Shard 上的 Chunk 数量。如果发现分布不均,Balancer 会自动迁移 Chunks,以确保数据在 Shards 之间均匀分布。
优点:
- 水平扩展: 可以通过增加 Shard 来扩展存储容量和读写吞吐量。
- 高可用性: 结合副本集,即使部分 Shard 或
mongos实例宕机,集群仍可继续服务。 - 提高查询性能: 查询可以并行地在多个 Shard 上执行。
缺点:
- 架构复杂性增加: 管理分片集群比单实例或副本集更复杂。
- 分片键选择关键: 不合适的分片键可能导致数据分布不均(热点问题)或查询效率低下(广播查询)。
- 某些操作受限或性能下降: 跨多个 Shard 的操作(如某些聚合、非分片键的查询)可能比在单个 Shard 上慢。
数据库设计与操作
Q: 数据库设计的三大范式是什么?它们解决了什么问题?
A: 数据库设计范式是指导如何组织数据库中表(关系)结构的一系列规则,主要目的是减少数据冗余、提高数据一致性、优化存储空间。
- 第一范式 (1NF - First Normal Form):
- 规则: 确保数据库表的每一列 (属性) 都是原子性的,不可再分。即每个字段只包含一个值,而不是列表或集合。
- 解决问题: 解决了数据项非原子性的问题,使得数据更易于查询和操作。
- 第二范式 (2NF - Second Normal Form):
- 规则: 首先必须满足 1NF。另外,表中非主键列必须完全依赖于整个主键,而不是只依赖于主键的一部分。(此规则主要针对复合主键)。如果一个表只有一个主键列,那么它只要满足 1NF 就自动满足 2NF。
- 解决问题: 解决了部分函数依赖导致的数据冗余和更新异常(插入异常、删除异常、修改异常)。例如,订单明细表 (订单ID, 产品ID, 产品名称, 数量),如果 (订单ID, 产品ID) 是复合主键,那么 "产品名称" 只依赖于 "产品ID",不完全依赖于整个主键,违反 2NF。应将产品信息拆分到单独的产品表中。
- 第三范式 (3NF - Third Normal Form):
- 规则: 首先必须满足 2NF。另外,表中非主键列之间不能存在传递依赖关系。即任何非主键列都不能依赖于其他非主键列。
- 解决问题: 解决了传递函数依赖导致的数据冗余和更新异常。例如,员工表 (员工ID, 姓名, 部门ID, 部门名称),"部门名称" 依赖于 "部门ID" (非主键),而 "部门ID" 依赖于 "员工ID" (主键),存在传递依赖。应将部门信息拆分到单独的部门表中。
总结: 范式的目标是消除冗余、保证数据一致性。但在实践中,有时为了查询性能,会进行反范式 (Denormalization) 设计,适当增加数据冗余(如冗余存储某些常用字段)来避免复杂的连接查询。需要在数据一致性和查询性能之间做权衡。
Q: 什么是 SQL 注入?如何防止?
A: SQL 注入 (SQL Injection) 是一种常见的网络攻击技术。攻击者通过在应用程序的用户输入(如表单、URL 参数)中注入恶意的 SQL 代码,欺骗应用程序执行非预期的数据库操作,从而达到窃取数据、篡改数据、甚至控制数据库服务器的目的。
示例: 假设登录查询 SQL 为:SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "' 如果用户输入的 username 为 admin' -- (注意最后的空格和注释符),密码任意,则拼接后的 SQL 变为: SELECT * FROM users WHERE username = 'admin' -- ' AND password = '...'-- 在 SQL 中是注释符,后面的语句被忽略,查询变成了只验证用户名 admin 是否存在,从而可能绕过密码验证。
如何防止 SQL 注入:
- 参数化查询 (Parameterized Queries) / 预编译语句 (Prepared Statements): 最有效的方法。应用程序将 SQL 模板(包含占位符)发送给数据库,然后单独发送用户输入的数据作为参数。数据库会区分代码和数据,用户输入的数据永远被当作数据处理,不会被解释为 SQL 命令。
- Node.js 中,
mysql或mysql2库都支持:connection.query('SELECT * FROM users WHERE id = ?', [userId], function(...) {}) - 其他 ORM 框架(如 Sequelize, TypeORM)通常默认使用参数化查询。
- Node.js 中,
- 输入验证与清理 (Input Validation and Sanitization):
- 验证: 对用户输入进行严格的格式、类型、长度校验。例如,用户 ID 应该是数字,邮箱应该符合格式。
- 清理/转义: 对用户输入中的特殊字符(如
',",;,--等)进行转义或移除,使其失去 SQL 语法意义。很多库提供了转义函数,但不建议单独依赖此方法,应优先使用参数化查询。
- 最小权限原则 (Least Privilege): 数据库连接账户只授予其完成任务所必需的最小权限。避免使用 root 或 admin 权限连接数据库。例如,只读操作的账户就不应该有写权限。
- Web 应用防火墙 (WAF): 使用 WAF 可以检测和拦截已知的 SQL 注入攻击模式。
- ORM 框架: 使用成熟的 ORM 框架通常能减少直接编写 SQL 的机会,并内置了防注入机制(基于参数化查询)。但需注意 ORM 提供的原生 SQL 查询接口仍可能存在风险。
- 错误信息处理: 不要将详细的数据库错误信息(如 SQL 语句、表结构)直接暴露给用户,这可能给攻击者提供线索。应返回通用的错误提示。
Q: 在什么情况下你会选择使用关系型数据库 (SQL),什么情况下选择 NoSQL 数据库?
A: 选择 SQL 还是 NoSQL 数据库取决于具体的应用需求、数据特性和系统目标。 选择关系型数据库 (SQL, 如 MySQL, PostgreSQL) 的情况:
- 数据结构化且稳定: 数据模式(表结构)相对固定,字段和关系明确。
- 需要强一致性 (ACID): 应用对数据的一致性要求非常高,事务处理是核心功能(如金融系统、订单系统)。
- 需要复杂查询和连接 (JOIN): 应用需要执行涉及多个表之间复杂关系的查询。SQL 在这方面非常强大和成熟。
- 数据量可控或垂直扩展可行: 数据增长在预期范围内,或者可以通过升级服务器硬件(垂直扩展)来满足性能需求。虽然 SQL 数据库也可以水平扩展(分库分表),但通常比 NoSQL 更复杂。
- 成熟的技术和生态: SQL 数据库有悠久的历史,拥有成熟的技术、丰富的工具、广泛的社区支持和大量经验丰富的开发人员。
选择 NoSQL 数据库 (如 MongoDB, Redis, Cassandra) 的情况:
- 数据结构灵活或不固定: 数据模式经常变化,或者需要存储非结构化、半结构化数据(如 JSON 文档、日志、用户生成内容)。
- 需要高扩展性和高吞吐量: 应用需要处理海量数据和/或极高的读写并发量,需要方便地进行水平扩展(如社交网络、物联网、大数据分析)。
- 对一致性要求相对较低: 可以接受最终一致性,或者只需要特定场景下的原子性(如单文档原子性)。(注意:一些 NoSQL 也开始提供更强的一致性选项或事务支持)。
- 特定数据模型优化: 需要利用 NoSQL 数据库针对特定数据模型(键值、文档、列族、图)的优化。例如:
- 键值存储 (Redis): 用于高速缓存、会话管理。
- 文档存储 (MongoDB): 用于内容管理、用户配置、目录。
- 列族存储 (Cassandra, HBase): 用于日志聚合、时间序列数据、写密集型应用。
- 图数据库 (Neo4j): 用于社交关系、推荐系统、知识图谱。
- 快速原型开发和迭代: 灵活的模式使得开发初期可以快速迭代,无需频繁修改数据库结构。
混合使用 (Polyglot Persistence): 在现代复杂的系统中,通常会根据不同业务场景的需求,混合使用 SQL 和 NoSQL 数据库,发挥各自的优势。例如,用户信息和订单核心数据可能存储在 MySQL 中,而产品目录、用户评论、用户活动流则可能存储在 MongoDB 或 Redis 中。
Q: 数据库连接池是什么?为什么需要它?
A: 数据库连接池 (Database Connection Pool) 是一种管理数据库连接的技术。它在应用程序启动时预先创建并维护一定数量的数据库连接,并将这些连接保存在一个“池”中。当应用程序需要访问数据库时,它不是直接创建新的连接,而是从池中获取一个空闲的连接。使用完毕后,连接不会立即关闭,而是归还到池中,供其他请求复用。
为什么需要数据库连接池:
- 性能提升:
- 减少连接建立开销: 建立和关闭数据库连接是相对耗时的操作(涉及网络握手、身份验证、资源分配等)。连接池通过复用现有连接,避免了频繁创建和销毁连接的开销,显著提高了应用程序的响应速度和数据库访问性能。
- 减少等待时间: 请求可以直接从池中获取可用连接,无需等待新连接建立。
- 资源管理:
- 控制连接数量: 数据库服务器能够处理的并发连接数是有限的。连接池可以限制应用程序创建的数据库连接总数,防止因连接过多而耗尽数据库服务器资源,导致服务不稳定或崩溃。
- 统一管理: 连接池提供了对数据库连接的集中管理,方便进行配置、监控和维护。
- 提高系统稳定性:
- 避免连接泄漏: 手动管理连接容易出现忘记关闭连接的情况,导致连接资源耗尽。连接池通常有机制来检测和回收长时间未使用的或无效的连接。
- 负载均衡 (部分实现): 某些高级连接池可以在多个数据库实例之间进行连接管理和负载分配。
工作流程:
- 初始化: 应用程序启动时,连接池根据配置创建一组初始连接(最小连接数)。
- 获取连接: 应用程序请求连接时,连接池检查是否有空闲连接:
- 有:返回一个空闲连接。
- 无,且当前连接数未达上限:创建新连接并返回。
- 无,且已达上限:请求进入等待队列,或根据配置返回错误。
- 使用连接: 应用程序使用获取到的连接执行数据库操作。
- 归还连接: 操作完成后,应用程序调用连接池的
release()或close()方法(具体名称因库而异),将连接归还到池中,标记为空闲状态,而不是真正关闭 TCP 连接。 - 维护: 连接池会定期检查连接的有效性,关闭无效连接,并根据需要创建新连接以维持最小连接数。
在 Node.js 中,常用的数据库驱动(如 mysql2, pg)通常都自带或推荐使用连接池库(如 mysql2 的 createPool, pg-pool)。