适合人群:每天在写 MySQL,听说过 NoSQL 但没系统用过 MongoDB 的 Java 开发者。
本文基于 MongoDB 7.x 讲解,概念对照统一以 MySQL 8 为参照。先记住最重要的一条边界:MongoDB 是通用文档数据库,它改变的是”数据怎么组织”,而不是”要不要数据库”。
一、从一个熟悉的需求说起
几乎每个后端都维护过一张会不断”长胖”的商品表。最初它可能只有名称、价格、库存三个字段:
1 | CREATE TABLE product ( |
很快需求来了:手机要展示颜色和内存;图书要有作者、ISBN 和出版时间;服装要区分尺码。这些属性彼此不同、未来还会继续增加,通用的做法通常有两条路:
- 垂直拆表:
product_base+ 一张扩展属性表(attr_key / attr_value),查询时自己拼装; - 加列:每次新需求就
ALTER TABLE加一个color、isbn、publish_time……没有该属性的行整列存NULL。
两条路都能走通,也都难受:前者把简单查询变成多次关联和程序拼装;后者让表越来越宽、稀疏列越来越多,历史遗留字段最后没人敢删。数据的形状明明是”每个商品一份完整描述”,却被关系范式切成了”一行 + 若干键值对 + 若干可空列”。
MongoDB 对这类问题的回答很直接:既然一条业务记录天然是一个整体,那就按文档存储它。上面三类商品可以共存于同一个集合(collection)中:
1 | // 数码类商品:嵌套规格对象 |
不需要预先声明共有列,也不需要为”暂时没有的属性”安排 NULL 占位——每份文档只描述自己的结构。这就是文档数据库最朴素的动机。
1.1 NoSQL 版图上的四大家族
NoSQL 不是一种技术,而是一组”放弃部分关系模型能力、换取特定场景优势”的方案。常见的四个分支可以按”数据长什么样”来记:
| 分支 | 代表产品 | 数据形态 | 典型用途 |
|---|---|---|---|
| 键值型(KV) | Redis | key → 简单值 | 缓存、计数器、分布式锁 |
| 文档型(Document) | MongoDB | 类 JSON 文档,支持嵌套与数组 | 业务主存储、内容管理、用户画像 |
| 宽列型(Wide-column) | Cassandra、HBase | 行键 + 大量动态列 | 日志流、海量写入的时序数据 |
| 图型(Graph) | Neo4j | 节点 + 边 | 社交关系、风控关联网络 |
MongoDB 属于文档型。它的特别之处在于通用性:Redis 通常做缓存放在链路旁边,Cassandra 和 Neo4j 服务特定形态的数据,而 MongoDB 有完整的索引、聚合、事务和多模扩展能力,足以承担”业务主存储”的角色——这是它与大多数 NoSQL 的本质区别。
1.2 别把它理解成”拒绝 SQL”
需要先澄清两个常见误读。第一,引入 MongoDB 不等于抛弃 MySQL:强事务场景(账户余额、订单状态机)依然适合放在关系库中。如果你还在纠结几种关系型数据库之间的差异,可以先读 MySQL、PostgreSQL(PG)与 Oracle:区别、SQL 对照与选型指南,那篇解决的是”选哪种 SQL”;本系列要补的是”什么时候连 SQL 都不必是默认答案”。
第二,MongoDB 与 Elasticsearch(ES)也不是竞争关系。两者的数据长得都像 JSON,但分工完全不同,我在 Elasticsearch 从零到一(01):全文检索与核心概念 里详细对比过:ES 是面向检索与分析构建的读模型,不承载事务事实;MongoDB 则是可以放心写入业务主数据的存储引擎。
1 | 下单/扣库存 → MySQL(强事务事实来源) |
一句话:MySQL 管”对不对”,MongoDB 管”数据怎么长”,ES 管”找得到吗”。
二、BSON:文档的真实模样
上一节的示例以 JSON 形式呈现,但 MongoDB 在磁盘和网络中的真实格式是 BSON(Binary JSON)。纯 JSON 只有 string、number、boolean、null、array、object 六种形态,对数据库来说太粗糙了:金额用什么精度?日期是字符串还是毫秒数?整数和浮点如何区分?
BSON 在保留 JSON 直观性的同时补充了类型系统,常用的类型与 Java 近似对应如下:
| BSON 类型 | 示例写法 | Java 侧近似类型 | 使用提醒 |
|---|---|---|---|
| String | "name" |
String |
UTF-8 编码 |
| Int32 / Int64 | 120 / NumberLong(1) |
Integer / Long |
驱动自动选择 |
| Double | 3.14 |
Double |
精度不可控 |
| Decimal128 | NumberDecimal("129.00") |
BigDecimal |
金额一律用它,避免浮点误差 |
| Date | ISODate("2026-08-27T02:00:00Z") |
java.util.Date / Instant |
内部存 UTC 毫秒数 |
| ObjectId | ObjectId("64f0a...") |
org.bson.types.ObjectId |
默认 _id 生成策略 |
| Array | [1, 2, 3] |
List<T> |
数组是一等公民,可直接建索引 |
| Document(嵌套) | { "color": "黑" } |
Map<String, Object> / 自定义 POJO |
支持多层嵌套 |
这份对照里有两点值得展开。
2.1 _id 与 ObjectId
每个文档都必须有一个 _id 字段作为集合内的唯一标识,相当于主键;写入时不指定的话,驱动会自动生成一个 ObjectId。它的 12 个字节并非随机数:
- 前 4 字节是秒级时间戳,因此 ObjectId 整体上随时间递增;
- 中间 5 字节是进程启动时生成的随机值,同一进程内不变;
- 最后 3 字节是自增计数器,保证同进程同秒内不重复。
趋势递增这个特性在后面讲索引时会再次出现:按 _id 建立的默认索引附近写入,局部性好;但它也意味着 _id 本身不适合直接当分片键(所有新写入都会打到最后一片),这是后话。
2.2 16 MB 上限:文档不是越大越好
单个文档最大 16 MB。听起来很大(足够放下一部中等小说),但设计上仍然要求避免无界的增长:比如把帖子的一百万条评论全塞进帖子文档的数组里,迟早会撞上限制并拖垮读写性能。”关联数据何时嵌入、何时引用”有一整套建模方法论,第 04 篇会专门讨论;现在只需记住结论——**文档描述的是”一条业务记录”,不是一个业务域的全部数据。**超大的二进制文件则有 GridFS 协议负责切块存储。
三、概念对照:先把词汇表对齐
从 MySQL 过来,最大的门槛不是语法而是词汇。下面这张映射表贯穿整个系列,建议保存备查:
| 关系型(MySQL) | MongoDB | 说明 |
|---|---|---|
| Database | Database | 库的概念一致 |
| Table | Collection(集合) | 集合无需预定义结构 |
| Row | Document(文档) | 一行 = 一个 BSON 文档 |
| Column | Field(字段) | 同一集合内字段可以不同 |
| Primary Key | _id |
未指定时自动生成 ObjectId |
| JOIN | 嵌入文档 / $lookup |
首选嵌入,其次管道联查 |
| GROUP BY | Aggregation Pipeline | 聚合管道,第 05、06 篇的主角 |
| 索引 Index | Index | 同样基于 B-Tree,语法不同 |
表格能帮助入门,但有三处不能画等号的地方,恰恰是初学者最容易踩坑的位置。
3.1 集合 ≠ 表:动态 Schema 的自由与纪律
关系表的每一行必须符合建表时声明的列结构;MongoDB 集合则不做强制——同一个集合里,昨天带 author 字段的书、今天新加 edition 字段的书可以并存。这带来一个巨大的工程红利:应用迭代新增字段不再依赖 DBA 执行 DDL,灰度发布期间新旧两种结构的文档也能和平共处。
自由不等于放任。实践中,生产环境几乎都会用 Schema 校验(JSON Schema Validator)为集合声明字段的类型约束,让”灵活”发生在规划内,而不是退化成无法维护的垃圾场。这与前端 TypeScript 的哲学异曲同工:动态的能力之上再加一层显式契约。
3.2 没有 Schema,不等于没有约束
字段结构是自由的,但以下约束依然存在且比 MySQL 更需要主动设计:
- 唯一性靠索引而非主键约束:除了
_id自动唯一,其他字段想唯一(如用户名)必须显式建唯一索引; - 引用完整性没有外键保证:A 文档引用 B 的
_id,删掉 B 时数据库不会拦截你,一致性要靠应用层或事务来维持; - 类型错位只有运行时才发现:往
price里写字符串不会报错,直到某天排序或聚合出错才暴露——这就是要用 Schema 校验的原因。
3.3 事务范围不一样
MySQL 的 InnoDB 把多表事务当作基本盘;MongoDB 则把单文档原子性当作基本盘——对一个文档内多个字段的修改天然是原子的,所以很多”需要事务”的场景在第 04 篇就被建模消解掉了。真正跨文档、跨集合的事务由 4.0 引入的多文档事务机制兜底,可用但有成本与前提(副本集环境、时长上限),第 08 篇专门展开。
四、三方对照:MySQL、MongoDB 与 Elasticsearch 的选型边界
词汇表对齐之后,最后一步是把三者放进同一张地图,确认各自的位置。
| 维度 | MySQL | MongoDB | Elasticsearch |
|---|---|---|---|
| 数据模型 | 行式表,Schema 先行 | BSON 文档,动态 Schema | 倒排索引之上的 JSON 文档 |
| 核心强项 | 强事务、复杂关联查询 | 灵活建模、高写入、分布式内建 | 全文检索、相关性排序、聚合分析 |
| 事务 | 多语句事务是基本盘 | 单文档原子;4.0+ 跨文档事务 | 无事务概念 |
| 写入可见性 | 提交即可见 | 提交即可见 | 近实时(默认约 1 秒 refresh) |
| 水平扩展 | 依靠分库分表等外挂方案 | 原生分片集群 | 原生分片 + 副本 |
| 典型角色 | 业务事实来源 | 灵活业务的现代主存储 | 搜索与分析读模型 |
从这张表可以提炼出一套务实的组合拳:
- 稳定核心走 MySQL:账户、余额、订单状态流转这些”错一分钱都是事故”的路径,享受成熟的事务与生态;
- 形态多变、读取密集的业务数据走 MongoDB:商品详情、内容系统、用户行为流水,既免去频繁 ALTER TABLE,又获得原生的水平扩展路径;
- 搜索体验交给 ES:把需要全文检索与筛选聚合的字段同步过去做成读模型,别逼着主存储去干搜索引擎的活。
三者不是升级替代的关系,而是各管一段。实际项目里最常见的架构,就是这三层各司其职的组合。
五、什么场景值得引入,哪些红线要想清楚
5.1 MongoDB 的舒适区
| 场景 | 为什么合适 |
|---|---|
| 商品目录 / 内容管理系统 | 属性因品类而异,嵌套描述一处取齐 |
| 用户画像 / 配置中心 | 字段持续演进,Schema 变更零成本 |
| 行为日志 / IoT 数据 | 写入吞吐高,配合桶模式做时序压缩 |
| 移动端 / 小程序后端 | 接口输出就是 JSON,前后端心智一致 |
| 快速迭代的原型 | 先跑起来再固化结构,试错成本低 |
| 需要平滑水平扩展的业务 | 分片集群内置,不必先分库分表 |
5.2 这些情况请三思
- 核心账务:高频跨实体强事务、严格的资金一致性审计,InnoDB 至今仍是更省心的选择;
- 重即席多表 JOIN 的报表:大量异构表临时关联的分析需求,关系库或 OLAP 引擎的表达力更强;
- 强约束合规场景:依赖数据库外键、CHECK 约束兜底数据正确性,而不是靠应用层纪律的团队,迁移前要先补齐这层能力;
- 只因为”听说更快”:任何存储换掉索引设计和访问模式谈性能都是空话,第 07 篇会用执行计划说话。
一条纪律:决定用 MongoDB 的理由应该是”我的数据形态适合文档模型”,而不是”MySQL 用腻了”。前者是选型,后者只会把碰巧合适的方案安在不合适的问题上。
六、总结与下一步
- MongoDB 是通用文档数据库,与 KV、宽列、图型 NoSQL 同属一族但定位不同:它具备承担业务主存储的完整能力;
- 磁盘格式是 BSON,在 JSON 之上补齐了 Decimal128、Date、ObjectId 等类型;金额用
Decimal128,超大文件另找 GridFS; - 从 MySQL 迁移认知时盯住三个”不能画等号”:集合无强制 Schema(用校验器补纪律)、除
_id外的唯一性和引用完整性都要自己设计、事务基本盘是单文档原子性; - 与 MySQL、ES 是分工而非替代:MySQL 管强事务事实,MongoDB 管灵活业务数据,ES 管检索读模型;
- ObjectId 天然趋势递增——好用,但当分片键会有热点问题,伏笔留给第 10 篇。
下一篇:《MongoDB 从零到一(02):Docker 环境搭建与 mongosh 快速上手》——用 Docker Compose 把 MongoDB 跑起来,安装 Compass 可视化工具,并把本文的示例数据亲手写进库里。
思考与练习
- 找一张你项目中字段最多的表,逐列判断:哪些属于”实体的固有属性”,哪些只是”某一小类的扩展属性”?后者换成文档模型后会落在哪里?
- 本文说”引用完整性没有外键保证”,试着列出你的业务里所有依赖外键拦截脏数据的场景,想一想换成 MongoDB 后分别由谁来兜底?
price字段为什么坚持用NumberDecimal("4999.00")而不是裸写4999.00?动手在 JS 里执行一下0.1 + 0.2再回来想想。