MongoDB 作为主流的文档型数据库,在很多业务场景中替代了传统关系型数据库。它采用 BSON 格式存储数据,支持嵌套文档与数组,这种结构让开发侧建模更贴近对象。但如果不了解其内部机制,很容易在性能、一致性和扩容上遇到问题。
一、文档模型与 BSON 存储原理
MongoDB 中的一条记录是一个文档,使用 BSON(Binary JSON)编码。BSON 在 JSON 基础上增加了日期、二进制、ObjectId 等类型,并以二进制方式存储,解析效率高于文本 JSON。文档不需要预先定义统一结构,同一个集合里的文档字段可以不同,这就是动态模式。
动态模式带来灵活性的同时也有代价。如果业务层不加以约束,可能出现同名字段类型不一致,导致索引失效或聚合出错。建议在应用层使用校验规则,例如通过 JSON Schema 限制集合的字段类型:
db.createCollection('user', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['name', 'age'],
properties: {
name: { bsonType: 'string' },
age: { bsonType: 'int', minimum: 0 }
}
}
}
});
上面代码在创建 user 集合时要求文档必须包含字符串类型的 name 和整数类型的 age。这样既能保留文档模型的弹性,又避免脏数据进入库里。BSON 文档在磁盘上连续存放,单文档最大为 16MB,设计时要避免把过大的数组塞进一个文档。
二、副本集与写关注机制
生产环境几乎都会部署副本集,由一个主节点和多个从节点组成。主节点接收写请求,并把操作记录到 oplog,从节点异步拉取 oplog 重放,实现数据复制。当主节点宕机,剩余节点会选举出新主节点,保证服务可用。
写关注(writeConcern)控制写操作要确认到几个节点才算成功。默认 w:1 表示主节点写入就返回,若主节点随后崩溃且未同步,数据可能丢失。对一致性要求高的场景可设置 w:'majority',即多数节点确认:
db.order.insertOne(
{ item: 'book', qty: 10 },
{ writeConcern: { w: 'majority', wtimeout: 5000 } }
);
w:'majority' 会降低写吞吐并增加延迟,但能防止脑裂导致的数据回滚。读关注(readConcern)和读偏好(readPreference)配合,可控制从哪个节点读、读到的数据是否已被多数节点确认。理解这三者的组合,才能在设计时平衡性能与可靠。
三、分片集群与片键选择
当数据量超过单机容量,需要使用分片集群。数据按片键(shard key)分散到不同分片。片键的选择直接决定集群是否均衡。如果片键是自增主键,所有新写都会落到同一个分片,形成热点。
好的片键应具备高基数且写分布均匀,例如用户 ID 的哈希。下面是开启哈希分片的示例:
sh.enableSharding('shop');
sh.shardCollection('shop.orders', { user_id: 'hashed' });
哈希片键让文档均匀落到各分片,适合写多读少的场景。但如果经常按范围查询某时间段订单,哈希键会导致广播查询。此时可考虑复合片键,把频繁范围查询的字段放在前面。分片后索引在每个分片独立维护,跨分片聚合需在路由层合并,复杂度高于副本集。
四、聚合管道的使用边界
MongoDB 的聚合管道由多个阶段组成,如 $match、$group、$sort,可替代关系库的连表与分组。管道在服务器端执行,减少应用层循环查询。
但聚合默认使用 100MB 内存,若阶段超出会报错,需要开启 allowDiskUse 落盘。以下示例统计每个用户的订单数:
db.orders.aggregate([
{ $match: { status: 'paid' } },
{ $group: { _id: '$user_id', total: { $sum: 1 } } },
{ $sort: { total: -1 } }
], { allowDiskUse: true });
把 $match 放在前面能利用索引减少待处理文档量。如果管道涉及多集合,可用 $lookup 做左连接,但它在分片集群中限制较多。复杂报表建议把结果缓存到另一个集合,避免每次实时聚合拖慢主库。
五、索引与查询计划
索引是 MongoDB 性能的核心。常用有单字段、复合、多键(数组)和文本索引。通过 explain 可查看查询是否走索引以及扫描文档数:
db.user.find({ age: { $gt: 20 } }).explain('executionStats');
复合索引遵循最左前缀原则,顺序应按查询频率和区分度排列。过多索引会拖慢写入并占用内存,可用 $indexStats 找出长期未用的索引并删除。另外,MongoDB 的覆盖查询(covered query)可只从索引返回字段,不加载文档,对高频小字段查询提升明显。
掌握上述机制后,再回头看 MongoDB 的设计,就不会只把它当成一个能存 JSON 的库。结合业务读写模型来规划文档结构、副本策略和分片方式,才能在规模增长时依然保持平稳。