MongoDB · 基础概念
1.1 是什么
MongoDB 是面向文档(Document-Oriented)的 NoSQL (Not Only SQL,非关系型数据库统称)数据库,用类 JSON 的 BSON 格式存储数据。
- 数据层级:
Database→Collection→Document→Field
(对比 MySQL:Database→Table→Row→Column) - 存储格式:BSON(Binary JSON),二进制、带类型和长度前缀
- 不要求固定表结构,同一集合内的文档字段可以不同
BSON vs JSON
| 维度 | JSON | BSON |
|---|---|---|
| 格式 | 文本 | 二进制 |
| 类型 | 基础类型 | 额外支持 Date / ObjectId(12字节,MongoDB默认主键) / Decimal128(高精度十进制) / Binary / Timestamp / 正则 |
| 解析 | 需全量解析 | 二进制格式下,每个文档和值都有长度前缀,比如字符串前面有 4 字节表示长度。不需要像 JSON 那样逐字符扫描,可以直接读到长度,然后跳过整个值。如果只需要查找某个字段,可以快速跳过不相关的字段,因此遍历和解析效率更高 |
| 体积 | 较小 | 通常略大(额外存了类型和长度) |
1.2 特点
| 特点 | 说明 |
|---|---|
| 文档模型 | 一条记录就是一个完整对象,天然贴合 Java 对象,减少 JOIN |
| 弱 Schema | 字段可动态增减,适合快速迭代;代价是需要自己约束数据质量 |
| 高读写吞吐 | 内存映射 + 文档级并发控制 + 顺序写 journal |
| 水平扩展 | 原生分片,加机器自动均衡,不依赖中间件 |
| 高可用 | 副本集自动故障转移,无需额外组件 |
| 丰富查询 | 支持二级索引、范围查询、聚合管道、地理空间、全文检索 |
| 多文档事务 | 4.0 起支持副本集事务,4.2 起支持分片集群事务 |
1.3 应用场景及优势
适合
- 字段不固定、结构频繁变化的业务(商品详情、配置、表单)
- 日志 / 埋点 / 监控数据(写入量大、按时间查询、可 TTL 清理)
- IM 消息、评论、Feed 流(天然文档结构 + 游标分页)
- 物联网 / 设备上报(高并发写入 + 地理空间查询)
- 内容管理、爬虫结果存储(嵌套结构多)
不适合
- 强事务、强一致的核心账务(金额、订单流水)
- 大量多表关联的复杂报表分析
- 需要复杂 JOIN + 聚合的 OLAP 场景
优势:以「单文档」为单位读写,用内嵌冗余换掉 JOIN,用原生分片换掉分库分表中间件。
1.4 与 MySQL 的对比记忆
| 维度 | MySQL | MongoDB |
|---|---|---|
| 数据模型 | 表 / 行 / 列,强 Schema | 集合 / 文档,弱 Schema |
| 索引结构 | B+ 树 | B 树 |
| 锁粒度 | 行锁 + MVCC | 文档级并发(乐观并发控制) |
| 事务 | 默认支持,成熟稳定 | 4.0+ 才支持,有性能损耗 |
| 关联 | JOIN | $lookup,性能差,靠内嵌冗余替代 |
| 扩展方式 | 垂直为主,分库分表靠中间件 | 原生分片,自动均衡 |
| 一致性 | 默认强一致 | 默认 CP,可调最终一致 |
| 单条记录上限 | 行约 65535 字节(不含 BLOB/TEXT) | 文档 16MB |
| 典型选型 | 强事务 / 复杂报表 / 核心账务 | 弱结构 / 高吞吐 / 日志 / 内容 / IM |
选型口诀:要事务、要关联、要报表 → MySQL;要吞吐、要弹性、要结构自由 → MongoDB。
1.5 文档
MongoDB 中最基本的数据单元,相当于关系型数据库中的一行记录。
文档是一个有序的键值对集合,用 BSON 存储,支持嵌套和数组。
特点:
- 结构灵活:不同文档的字段可以完全不同,无需固定表结构。
- 支持嵌套:字段的值可以是另一个文档、数组,甚至嵌套多层。
- 主键
_id:每个文档必须有一个唯一的_id字段,默认自动生成 ObjectId(12 字节唯一 ID 类型)。
示例:
json
{
"_id": ObjectId("5f8d8c8b8f8b8b8b8b8b8b8b"),
"name": "Alice",
"age": 30,
"address": {
"city": "Beijing",
"province": "Beijing"
},
"hobbies": ["reading", "coding"]
}
一句话总结:文档是MongoDB 的基本数据单元,类似关系型数据库的一行,但字段可以动态变化。用 BSON 存储,支持嵌套和数组。
1.6 集合
文档的分组,相当于关系型数据库中的表。
一个集合中可以存放结构不同的文档。
特点:
- 无模式(Schema-less):不需要预先定义字段和类型,写入时动态决定。
- 命名规则:不能以
system.开头,不能包含$和空字符,区分大小写。 - 动态创建:向一个不存在的集合插入文档时,MongoDB 会自动创建它(若没有开启多文档事务会自动创建,开启多文档事务后最好手动创建)。
与 MySQL 表的对比:
| 维度 | MySQL 表 | MongoDB 集合 |
|---|---|---|
| 结构 | 固定列、行 | 无固定结构,文档可不同 |
| 修改结构 | ALTER TABLE |
无需修改,直接写入新字段 |
| 数据单元 | 行 | 文档 |
一句话总结:文档的容器,类似表。但它没有固定结构,字段可以随文档变化。第一次插入数据时会自动创建集合
1.7 ObjectId
MongoDB 文档默认的 _id 类型,是一个 12 字节的 BSON 对象,用于在分布式环境中生成全局唯一 ID。
12 字节结构:
| 字节 | 含义 |
|---|---|
| 0-3 | Unix 时间戳(秒级) |
| 4-6 | 机器标识(主机名哈希) |
| 7-8 | 进程 ID |
| 9-11 | 随机增量计数器 |
特点:
- 趋势递增:前 4 字节是时间戳,因此 ObjectId 大致按创建时间递增,对索引友好。
- 分布式唯一:不同机器、不同进程生成的 ObjectId 不会冲突。
- 无需中央协调:不需要像 MySQL 自增主键那样依赖数据库单点。
- 客户端可生成:驱动程序可以在客户端生成,减少数据库压力。
与 MySQL 自增主键对比:
| 维度 | MySQL 自增主键 | MongoDB ObjectId |
|---|---|---|
| 生成方式 | 数据库集中生成 | 客户端/服务端独立生成 |
| 分布式扩展 | 需要分片或步长调整 | 天然分布式唯一 |
| 有序性 | 严格递增 | 趋势递增 |
| 类型 | 整数 | 12 字节二进制 |
| 空间占用 | 4/8 字节 | 12 字节 |
一句话总结:MongoDB 默认的主键类型,12 字节,包含时间戳、机器标识、进程 ID 和随机计数器。它保证分布式唯一且趋势递增,适合分片和水平扩展。相比 MySQL 自增主键,它不依赖中心节点,但占用空间更大。
1.8 增删改查常见写法
mongosh
javascript
// ---------- 增 ----------
db.user.insertOne({ name: "张三", age: 25, tags: ["vip", "new"] })
db.user.insertMany([{ name: "李四", age: 30 }, { name: "王五", age: 17 }])
// ---------- 查 ----------
db.user.find({ age: { $gte: 18 } }).sort({ age: -1 }).limit(10)
db.user.findOne({ _id: ObjectId("650000000000000000000001") })
db.user.find({ tags: "vip" }) // 数组包含某元素
db.user.find({ tags: { $all: ["vip", "new"] } }) // 数组同时包含多个
db.user.find({ tags: { $size: 2 } }) // 数组长度
db.user.find({ tags: { $elemMatch: { $eq: "vip" } } }) // 数组元素多条件
db.user.find({ "addr.city": "杭州" }) // 嵌套字段(点号)
db.user.find({ $or: [{ age: 18 }, { name: "张三" }] })
db.user.find({ age: { $in: [18, 19, 20] } })
db.user.find({ name: { $regex: "^张" } }) // 前缀匹配才走索引
db.user.find({}, { name: 1, age: 1, _id: 0 }) // 投影(1 包含 / 0 排除,不能混用)
db.user.countDocuments({ age: { $gt: 18 } })
// ---------- 改 ----------
db.user.updateOne({ name: "张三" }, { $set: { age: 26 } })
db.user.updateMany({ age: { $lt: 18 } }, { $set: { type: "minor" } })
db.user.updateOne({ name: "张三" }, { $inc: { score: 10 }, $push: { tags: "vvip" } })
db.user.updateOne({ name: "李四" }, { $set: { age: 30 } }, { upsert: true }) // 不存在则插入
db.user.replaceOne({ name: "张三" }, { name: "张三", age: 26 }) // 整体替换
// ---------- 删 ----------
db.user.deleteOne({ name: "张三" })
db.user.deleteMany({ age: { $lt: 18 } })
db.user.drop() // 删整个集合
Java(Spring Data MongoDB)
java
// ---------- 增 ----------
mongoTemplate.insert(user);
mongoTemplate.insert(userList, User.class);
// ---------- 查 ----------
Query q = Query.query(Criteria.where("age").gte(18))
.with(Sort.by(Sort.Direction.DESC, "age"))
.limit(10);
List<User> list = mongoTemplate.find(q, User.class);
// -----------根据查询条件查询指定字段---------------
Query query = new Query();
query.addCriteria(Criteria.where("status").is("ACTIVE")
.and("age").gte(18)); // 1. 构建查询条件
query.fields()
.include("name")
.include("age")
.exclude("_id"); // 2. 指定返回字段(只返回 name 和 age,不返回 _id)
List<User> users = mongoTemplate.find(query, User.class, "user"); // 3. 执行查询,映射到指定实体或 Map
List<Map> maps = mongoTemplate.find(query, Map.class, "user"); // 或者返回 Map
// ---------- 改 ----------
Update u = new Update().set("age", 26).inc("score", 10).push("tags", "vvip");
mongoTemplate.updateFirst(Query.query(Criteria.where("name").is("张三")), u, User.class);
mongoTemplate.updateMulti(Query.query(Criteria.where("age").lt(18)), u, User.class);
mongoTemplate.upsert(Query.query(Criteria.where("name").is("李四")), u, User.class);
// ---------- 删 ----------
mongoTemplate.remove(Query.query(Criteria.where("name").is("张三")), User.class);