MongoDB 与 NoSQL 数据库:文档数据库入门到实战

2024-09-15 后端

1. NoSQL 数据库概述

1.1 为什么需要 NoSQL

传统关系型数据库(如 MySQL、PostgreSQL、Oracle)在过去数十年间一直是数据存储的基石。它们以严格的结构化表(Table)、预定义 Schema、ACID 事务和强大的 SQL 查询著称。然而,随着互联网规模的爆炸式增长,关系型数据库逐渐暴露出以下局限:

局限 说明
Schema 僵化 每次新增字段都需要执行 ALTER TABLE,在大表上可能锁表数小时
扩展困难 垂直扩展(升级硬件)有物理上限,水平扩展(分库分表)需要中间件且复杂度高
大数据性能瓶颈 多表 JOIN 在海量数据下性能急剧下降
非结构化数据支持差 JSON、大文本、地理位置等类型处理不自然
开发效率低 ORM 映射增加心智负担,模式变更需要 DBA 介入

例如,一个社交平台的海量用户动态、行为日志、实时计数器等场景,用关系型数据库处理会非常痛苦。这正是 NoSQL(Not Only SQL)数据库崛起的背景。

1.2 四类 NoSQL 数据库

NoSQL 并非单一技术,而是涵盖四大类数据模型:

类别 代表产品 数据模型 典型场景
文档型 MongoDB, Couchbase JSON/BSON 文档 内容管理、日志、用户资料、电商商品
键值型 Redis, DynamoDB, etcd 键值对 缓存、会话管理、配置中心
列族型 Cassandra, HBase 宽列存储 时序数据、IoT、推荐引擎、消息队列
图型 Neo4j, Neptune 节点+边 社交关系、推荐系统、知识图谱、欺诈检测

1.3 CAP 定理与 BASE

理解 NoSQL 必须先理解两个核心理论。

CAP 定理(Brewer's Theorem):分布式系统只能同时满足以下三者中的两个:

  • C(一致性):所有节点在同一时刻看到相同数据
  • A(可用性):每个请求都能获得非错误的响应
  • P(分区容错性):系统在网络分区时仍能继续工作

由于网络分区不可避免,实际上是在 CP 与 AP 之间做选择:

CP 系统(如 HBase、etcd)—— 牺牲可用性保证一致性
AP 系统(如 Cassandra、DynamoDB)—— 牺牲一致性保证可用性
CA 系统(单机 MySQL)—— 实际上无法在网络分区时工作

MongoDB 在副本集模式下默认为 CP(主节点写入,从节点异步复制),但可配置写关注(write concern)和读关注(read concern)来调节一致性级别。

BASE 理论:NoSQL 对 ACID 的替代方案

  • BA(Basically Available):基本可用
  • S(Soft State):允许中间状态
  • E(Eventually Consistent):最终一致

"If you have ACID, you sleep well at night. If you have BASE, you ship on Friday."
— NoSQL 领域的笑谈

1.4 MongoDB 在 NoSQL 生态中的位置

MongoDB 是目前最流行的文档型数据库,由 MongoDB Inc.(原 10gen)于 2009 年开源。它在 NoSQL 生态中定位如下:

  • 数据模型:BSON(Binary JSON)文档,天然支持嵌套对象和数组
  • 查询能力:丰富的查询语言 + 聚合管道(Aggregation Pipeline),媲美 SQL 的灵活度
  • 一致性:支持可调一致性(不同读写级别)
  • 扩展性:原生分片(Sharding),自动均衡数据
  • 生态:MongoDB Atlas 云服务、Mongoose/Spring Data/Mongoid 等驱动

MongoDB 的竞争对手包括 Firestore(Firebase)、Couchbase、Amazon DocumentDB,但 MongoDB 在独立部署和自运维场景中仍居主导地位。


2. MongoDB 基础

2.1 Docker 安装

最快捷的本地实践方式(本文章所有示例均在以下环境验证):

# 拉取 MongoDB 8.0 镜像
docker pull mongo:8.0

# 启动单节点 MongoDB
docker run -d \
  --name mongo-local \
  -p 27017:27017 \
  -v mongo-data:/data/db \
  -e MONGO_INITDB_ROOT_USERNAME=admin \
  -e MONGO_INITDB_ROOT_PASSWORD=admin123 \
  mongo:8.0

# 进入 mongosh 交互 Shell
docker exec -it mongo-local mongosh -u admin -p admin123

# 或者使用连接字符串直连
# mongodb://admin:admin123@localhost:27017

2.2 核心概念

MongoDB 的数据结构非常直观:

┌────────────────────────────────────────┐
│  Database (数据库)                     │
│  ┌──────────────────────────────────┐  │
│  │  Collection (集合)               │  │
│  │  ┌─────────────────────────────┐ │  │
│  │  │  Document (文档) — JSON 对象 │  │  │
│  │  │  {                          │  │  │
│  │  │    _id: ObjectId(...),      │  │  │
│  │  │    name: "Alice",           │  │  │
│  │  │    age: 30,                 │  │  │
│  │  │    tags: ["dev", "ops"]     │  │  │
│  │  │  }                          │  │  │
│  │  └─────────────────────────────┘ │  │
│  └──────────────────────────────────┘  │
└────────────────────────────────────────┘

2.3 与关系型数据库概念对比

RDBMS(MySQL) MongoDB 说明
Database Database 相同概念
Table Collection 集合不强制固定 Schema
Row Document BSON 文档,可嵌套
Column Field 字段可以在不同文档中不同
Primary Key _id 自动创建,默认 ObjectId
Index Index 基本相同
JOIN $lookup / 嵌入式文档 推荐嵌入式避免 JOIN
Transactions 支持(4.0+) 多文档事务

关键差异:

// RDBMS:每张表需要 CREATE TABLE 定义 Schema
CREATE TABLE users (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(100),
  age INT
);

// MongoDB:直接插入文档,无需预定义 Schema
db.users.insertOne({
  name: "Alice",
  age: 30,
  address: { city: "北京", district: "海淀" },  // 嵌套对象
  tags: ["开发者", "MongoDB"]                    // 数组
})

// 第二个文档可以和第一个字段不同!
db.users.insertOne({
  name: "Bob",
  phone: "13800138000",  // 新增字段
  meta: { lastLogin: ISODate() }
})

2.4 mongosh 基本使用

// 查看所有数据库
show dbs
// 或
db.adminCommand('listDatabases')

// 切换/创建数据库(use 后自动创建)
use myblog

// 查看所有集合
show collections

// 查看数据库信息
db.stats()

// 查看集合信息
db.articles.stats()

// 查看集合中所有文档
db.articles.find().pretty()

// 查看帮助
help
db.help()
db.articles.help()

// 列出所有可用的方法
db.articles.getDB()

2.5 BSON 数据类型

MongoDB 使用 BSON(Binary JSON)存储文档,比 JSON 支持更多数据类型:

BSON 类型 数字编号 示例 说明
Double 1 3.14159 双精度浮点数
String 2 "你好" UTF-8 字符串
Object 3 { a: 1 } 内嵌文档
Array 4 [1,2,3] 数组
Binary 5 BinData(0, ...) 二进制数据
ObjectId 7 ObjectId("...") 12 字节唯一 ID
Boolean 8 true/false 布尔值
Date 9 ISODate("2024-09-15") 64 位整数毫秒时间戳
Null 10 null 空值
Int32 16 NumberInt(42) 32 位整数
Timestamp 17 Timestamp(1726,...) 64 位时间戳(内部使用)
Int64 18 NumberLong(999) 64 位整数
Decimal128 19 NumberDecimal("10.99") 高精度十进制(财务场景)
// 在 mongosh 中测试 BSON 类型
db.types.insertOne({
  stringField: "文本",
  intField: NumberInt(42),
  longField: NumberLong(9999999999),
  decimalField: NumberDecimal("99.99"),
  doubleField: 3.14159,
  dateField: new Date(),
  isActive: true,
  nested: { key: "value" },
  arr: [1, "混合", true, { a: 1 }],
  nullField: null,
  objectIdField: new ObjectId(),
  binaryField: BinData(0, "AAAA"),
  timestampField: Timestamp(1726358400, 1)
})

3. CRUD 操作

3.1 Insert

insertOne

use shop

db.products.insertOne({
  name: "无线蓝牙耳机",
  category: "电子产品",
  price: NumberDecimal("299.00"),
  stock: 150,
  specs: {
    brand: "SoundPro",
    color: "黑色",
    battery: "8小时"
  },
  tags: ["蓝牙", "无线", "降噪"],
  createdAt: new Date()
})

返回值包含 acknowledged: true 和 insertedId:

{
  acknowledged: true,
  insertedId: ObjectId("66e5a1b2c3d4e5f6a7b8c9d0")
}

insertMany

db.products.insertMany([
  {
    name: "机械键盘",
    category: "电子产品",
    price: NumberDecimal("599.00"),
    stock: 80,
    specs: { brand: "KeyCraft", layout: "87键", switch: "青轴" },
    tags: ["键盘", "机械", "办公"]
  },
  {
    name: "瑜伽垫",
    category: "运动用品",
    price: NumberDecimal("129.00"),
    stock: 300,
    specs: { brand: "FitLife", thickness: "6mm", material: "TPE" },
    tags: ["瑜伽", "健身", "防滑"]
  },
  {
    name: "不锈钢保温杯",
    category: "厨具",
    price: NumberDecimal("89.00"),
    stock: 500,
    specs: { brand: "WarmHeld", capacity: "500ml", material: "304不锈钢" },
    tags: ["水杯", "保温", "户外"]
  }
])

有序插入 vs 无序插入:

// ordered: true — 遇到错误停止后续插入(默认)
db.products.insertMany([
  { _id: "a", name: "A" },
  { _id: "a", name: "B" },  // 重复 _id,报错,B 不插入
  { _id: "c", name: "C" }   // 因为有序,C 也不插入
], { ordered: true })

// ordered: false — 跳过错误继续插入
db.products.insertMany([
  { _id: "x", name: "X" },
  { _id: "x", name: "Y" },  // 重复 _id,跳过
  { _id: "z", name: "Z" }   // 继续插入成功
], { ordered: false })

3.2 Query

find 与 findOne

// 查询所有文档
db.products.find()

// 美化输出
db.products.find().pretty()

// 查询一条
db.products.findOne({ name: "机械键盘" })

// 投影 — 只返回指定字段
db.products.find(
  { category: "电子产品" },
  { name: 1, price: 1, _id: 0 }  // 1=包含, 0=排除
)

查询操作符

比较操作符:

// $eq — 等于
db.products.find({ price: NumberDecimal("299.00") })
// 或 $eq 显式写法
db.products.find({ price: { $eq: NumberDecimal("299.00") } })

// $ne — 不等于
db.products.find({ category: { $ne: "电子产品" } })

// $gt / $gte — 大于 / 大于等于
db.products.find({ price: { $gt: NumberDecimal("100.00") } })
db.products.find({ price: { $gte: NumberDecimal("100.00") } })

// $lt / $lte — 小于 / 小于等于
db.products.find({ stock: { $lt: 100 } })
db.products.find({ stock: { $lte: 150 } })

// $in — 在列表中
db.products.find({ category: { $in: ["电子产品", "运动用品"] } })

// $nin — 不在列表中
db.products.find({ category: { $nin: ["厨具"] } })

逻辑操作符:

元素与数组操作符:

// $exists — 字段是否存在
db.products.find({ specs.color: { $exists: true } })

// $type — 按 BSON 类型查询
db.products.find({ price: { $type: "decimal" } })

// $regex — 正则匹配
db.products.find({ name: { $regex: /键盘/ } })
db.products.find({ name: { $regex: "^无", $options: "i" } })  // i 忽略大小写

// 数组查询
// 精确匹配数组
db.products.find({ tags: ["蓝牙", "无线", "降噪"] })

// 包含某个元素
db.products.find({ tags: "蓝牙" })

// $all — 包含所有指定元素
db.products.find({ tags: { $all: ["无线", "降噪"] } })

// $size — 数组长度
db.products.find({ tags: { $size: 3 } })

// $elemMatch — 数组内嵌文档匹配
db.products.find({ tags: { $elemMatch: { $eq: "蓝牙" } } })

嵌套文档查询:

// 点号访问嵌套字段(必须用引号)
db.products.find({ "specs.brand": "SoundPro" })

// 精确匹配嵌套文档
db.products.find({ specs: { brand: "SoundPro", color: "黑色", battery: "8小时" } })
// 注意:字段顺序和值必须完全一致!

3.3 Update

updateOne — 更新单条

// $set — 设置字段
db.products.updateOne(
  { name: "无线蓝牙耳机" },
  { $set: { price: NumberDecimal("279.00"), "specs.color": "白色" } }
)

// $unset — 删除字段
db.products.updateOne(
  { name: "无线蓝牙耳机" },
  { $unset: { "specs.battery": "" } }
)

// $inc — 自增/自减(原子操作)
db.products.updateOne(
  { name: "机械键盘" },
  { $inc: { stock: -1 } }  // 减1(下单扣库存)
)

// $mul — 相乘
db.products.updateOne(
  { name: "瑜伽垫" },
  { $mul: { price: NumberDecimal("0.9") } }  // 打九折
)

// $min / $max — 条件更新
db.products.updateOne(
  { name: "不锈钢保温杯" },
  { $min: { price: NumberDecimal("79.00") } }  // 如果当前 > 79 则更新
)

updateMany — 批量更新

// 所有电子产品涨价 10%
db.products.updateMany(
  { category: "电子产品" },
  { $mul: { price: NumberDecimal("1.10") } }
)

// 所有库存不足 100 的商品标记为低库存
db.products.updateMany(
  { stock: { $lt: 100 } },
  { $set: { status: "low_stock" } }
)

数组更新操作

upsert — 不存在则插入

// upsert: true — 文档不存在则创建
db.products.updateOne(
  { name: "智能手表" },
  {
    $setOnInsert: {
      category: "电子产品",
      price: NumberDecimal("1299.00"),
      stock: 60,
      created_at: new Date()
    },
    $set: { updated_at: new Date() }
  },
  { upsert: true }
)

3.4 Delete

// deleteOne — 删除单个文档
db.products.deleteOne({ name: "智能手表" })

// deleteMany — 删除多个
db.products.deleteMany({ stock: { $eq: 0 } })

// 删除所有文档(保留集合)
db.products.deleteMany({})

// 删除集合(整个集合及索引)
db.products.drop()

// 删除数据库
db.dropDatabase()

3.5 批量操作(bulkWrite)

bulkWrite 允许在一次网络往返中执行多种写入操作:


4. 聚合管道

聚合管道(Aggregation Pipeline)是 MongoDB 最强大的数据加工工具。数据依次经过每个阶段(Stage),每个阶段对文档进行变换。

类比 Unix 管道:cat log | grep ERROR | sort | uniq -c | head -10

4.1 基础阶段

准备示例数据:

db.orders.insertMany([
  { _id: 1, customer: "Alice", items: ["鼠标", "键盘"], total: 898, status: "completed", date: ISODate("2024-01-15") },
  { _id: 2, customer: "Bob", items: ["显示器"], total: 2399, status: "completed", date: ISODate("2024-01-20") },
  { _id: 3, customer: "Alice", items: ["硬盘"], total: 599, status: "pending", date: ISODate("2024-02-10") },
  { _id: 4, customer: "Carol", items: ["耳机", "鼠标垫"], total: 199, status: "completed", date: ISODate("2024-02-15") },
  { _id: 5, customer: "Bob", items: ["键盘", "音箱"], total: 1298, status: "shipped", date: ISODate("2024-03-05") },
  { _id: 6, customer: "Alice", items: ["笔记本"], total: 5999, status: "completed", date: ISODate("2024-03-20") }
])

$match — 筛选(类似 WHERE)

db.orders.aggregate([
  { $match: { status: "completed" } }
])

$group — 分组聚合(类似 GROUP BY)

// 按客户分组,计算总消费和订单数
db.orders.aggregate([
  { $group: {
    _id: "$customer",
    totalSpent: { $sum: "$total" },
    orderCount: { $sum: 1 },
    avgAmount: { $avg: "$total" },
    minAmount: { $min: "$total" },
    maxAmount: { $max: "$total" }
  } }
])

$sort — 排序

db.orders.aggregate([
  { $group: {
    _id: "$customer",
    totalSpent: { $sum: "$total" }
  }},
  { $sort: { totalSpent: -1 } }   // -1 降序, 1 升序
])

$project — 投影/字段变换(类似 SELECT)

db.orders.aggregate([
  { $project: {
    customer: 1,
    total: 1,
    date: { $dateToString: { format: "%Y-%m-%d", date: "$date" } },
    status: 1,
    itemsCount: { $size: "$items" },
    // 创建新计算字段
    totalCategory: {
      $cond: {
        if: { $gte: ["$total", 1000] },
        then: "大额订单",
        else: "普通订单"
      }
    },
    _id: 0
  } }
])

组合管道示例

// 找出消费总额最高的客户及其订单统计
db.orders.aggregate([
  // 1. 筛选已完成订单
  { $match: { status: { $in: ["completed", "shipped"] } } },

  // 2. 按客户分组统计
  { $group: {
    _id: "$customer",
    totalSpent: { $sum: "$total" },
    orderCount: { $sum: 1 },
    avgOrderValue: { $avg: "$total" }
  }},

  // 3. 只保留总消费 >= 1000 的客户
  { $match: { totalSpent: { $gte: 1000 } } },

  // 4. 按总消费降序排列
  { $sort: { totalSpent: -1 } },

  // 5. 格式化输出
  { $project: {
    customer: "$_id",
    totalSpent: { $round: ["$totalSpent", 2] },
    orderCount: 1,
    avgOrderValue: { $round: ["$avgOrderValue", 2] },
    _id: 0
  }}
])

4.2 $lookup — 关联查询(类似 JOIN)

准备关联集合:

db.customers.insertMany([
  { _id: "Alice", email: "alice@example.com", city: "北京", vip: true },
  { _id: "Bob", email: "bob@example.com", city: "上海", vip: false },
  { _id: "Carol", email: "carol@example.com", city: "深圳", vip: true }
])

db.order_details.insertMany([
  { orderId: 1, product: "鼠标", qty: 1, price: 199 },
  { orderId: 1, product: "键盘", qty: 1, price: 699 },
  { orderId: 2, product: "显示器", qty: 1, price: 2399 },
  { orderId: 6, product: "笔记本", qty: 1, price: 5999 }
])
// 关联 orders 与 customers
db.orders.aggregate([
  { $lookup: {
    from: "customers",                     // 关联集合名
    localField: "customer",                // 本地字段
    foreignField: "_id",                   // 对方字段
    as: "customerInfo"                     // 输出数组字段名
  }},
  // $lookup 结果是一个数组,用 $unwind 展开
  { $unwind: {
    path: "$customerInfo",
    preserveNullAndEmptyArrays: true       // 保留没有客户信息的订单
  }},
  { $project: {
    orderId: "$_id",
    customer: 1,
    total: 1,
    city: "$customerInfo.city",
    isVip: "$customerInfo.vip",
    email: "$customerInfo.email",
    _id: 0
  }}
])

多表关联 + 聚合:

4.3 高级阶段

$unwind — 展开数组

// 将每个订单按商品展开为多行
db.orders.aggregate([
  { $unwind: "$items" },
  { $project: {
    customer: 1,
    item: "$items",
    total: 1,
    _id: 0
  }}
])

// 结果:
// { customer: "Alice", item: "鼠标", total: 898 }
// { customer: "Alice", item: "键盘", total: 898 }
// { customer: "Bob",   item: "显示器", total: 2399 }
// ...
// 统计哪个商品被购买最多
db.orders.aggregate([
  { $unwind: "$items" },
  { $group: {
    _id: "$items",
    purchaseCount: { $sum: 1 }
  }},
  { $sort: { purchaseCount: -1 } }
])

$bucket — 分桶

// 将订单按金额分桶
db.orders.aggregate([
  { $bucket: {
    groupBy: "$total",
    boundaries: [0, 200, 500, 1000, 5000, 10000],
    default: "其他",
    output: {
      count: { $sum: 1 },
      orders: { $push: { id: "$_id", customer: "$customer" } },
      avgTotal: { $avg: "$total" }
    }
  }}
])

$facet — 多面聚合

4.4 完整聚合示例:电商报表


5. 索引

没有索引的查询就是"全表扫描"(Collection Scan),在数据量大时性能灾难。MongoDB 使用 B-Tree 索引结构。

5.1 单字段索引

// 创建单字段升序索引
db.products.createIndex({ price: 1 })    // 1=升序, -1=降序

// 查看集合索引
db.products.getIndexes()

// 结果示例
[
  { v: 2, key: { _id: 1 }, name: "_id_" },
  { v: 2, key: { price: 1 }, name: "price_1" }
]

// 唯一索引
db.products.createIndex({ name: 1 }, { unique: true })

// 稀疏索引(只索引包含该字段的文档)
db.products.createIndex({ email: 1 }, { sparse: true })

5.2 复合索引

// 复合索引:先按 category 排序,再按 price 排序
db.products.createIndex({ category: 1, price: -1 })

// 复合索引的"最左前缀"原则:
// 以下查询可以使用该索引:
db.products.find({ category: "电子产品" })
db.products.find({ category: "电子产品", price: { $gt: 100 } })

// 以下查询不能使用该索引(没有 category 条件):
db.products.find({ price: { $gt: 100 } })

// 复合索引设计建议
// 1. 等值条件字段放前面
// 2. 排序字段放后面
// 3. 范围条件字段放最后

5.3 多键索引(数组字段)

当被索引的字段是数组时,MongoDB 自动创建多键索引(Multikey Index):

// 自动创建多键索引
db.products.createIndex({ tags: 1 })

// 多键索引对以下查询有效
db.products.find({ tags: "蓝牙" })
db.products.find({ tags: { $all: ["无线", "降噪"] } })

// 注意:一个复合索引中只能有一个数组字段
// 下面的创建会报错(两个数组字段)
// db.products.createIndex({ tags: 1, items: 1 })  // ❌

5.4 文本索引

用于全文搜索:

// 在 name 和 specs.brand 字段上创建文本索引
db.products.createIndex(
  { name: "text", "specs.brand": "text" },
  { weights: { name: 10, "specs.brand": 5 } }  // name 权重更高
)

// $text 查询
db.products.find({ $text: { $search: "蓝牙耳机" } })

// 按文本相关性排序
db.products.find(
  { $text: { $search: "键盘机械" } },
  { score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } })

// 中文分词 — 需要额外配置(mongosh 中需安装 ICU 或 Elasticsearch 配合)
// MongoDB 文本索引默认支持的语言不包括中文

5.5 Geospatial 索引(2dsphere)

5.6 TTL 索引(自动过期)

// 创建会话集合,30分钟后自动删除
db.sessions.createIndex(
  { createdAt: 1 },
  { expireAfterSeconds: 1800 }   // 1800秒 = 30分钟
)

// 插入数据(必须包含 createdAt 日期字段)
db.sessions.insertOne({
  sessionId: "abc123",
  userId: "user_001",
  createdAt: new Date()       // TTL 索引基于此字段
})

// 自定义过期时间(使用 expireAfterSeconds: 0)
// 文档中指定 expireAt 字段
db.coupons.createIndex(
  { expireAt: 1 },
  { expireAfterSeconds: 0 }
)

db.coupons.insertOne({
  code: "DISCOUNT50",
  discount: 50,
  expireAt: new Date("2024-12-31T23:59:59Z")   // 过期时间精确到秒
})

// 说明:TTL 索引每隔 60 秒运行一次后台清理任务

5.7 索引优化与 explain()

索引使用总结:

索引类型 创建命令 适用场景
单字段索引 { field: 1 } 等值/范围查询
复合索引 { a:1, b:-1 } 多条件查询
多键索引 { arrayField: 1 } 数组字段查询
文本索引 { field: "text" } 全文搜索
地理索引 { loc: "2dsphere" } 位置查询
TTL 索引 { t: 1 }, { expireAfterSeconds: N } 自动过期数据
唯一索引 { f: 1 }, { unique: true } 约束唯一性
稀疏索引 { f: 1 }, { sparse: true } 只索引含该字段的文档
哈希索引 { f: "hashed" } 分片键(均衡分布)
部分索引 { f: 1 }, { partialFilterExpression: { ... } } 只索引部分文档

6. 复制集与分片

6.1 复制集(Replica Set)

复制集是 MongoDB 的高可用方案,由一主多从组成。

复制集架构

┌─────────────┐    选举      ┌─────────────┐
│  Primary    │◄────────────│  Secondary   │
│  (主节点)    │              │  (从节点1)   │
│  读写操作    │ ───────────►│  异步复制    │
└──────┬──────┘   oplog     └─────────────┘
       │                     ┌─────────────┐
       │                     │  Secondary   │
       └────────────────────►│  (从节点2)   │
           异步复制          │  异步复制    │
                             └─────────────┘

主从切换与读写分离

读写分离配置

// 主节点写入,从节点读取(需要设置 readPreference)
// 在 mongosh 中:
rs.secondaryOk()  // 允许从从节点读取

// 在应用代码驱动中:
// readPreference=secondary         // 始终读从节点
// readPreference=primaryPreferred  // 优先读主节点,主节点不可用读从节点

// 写关注(Write Concern)
// w: "majority" — 等待大多数节点确认写入
db.orders.insertOne(
  { item: "商品", qty: 100 },
  { writeConcern: { w: "majority", wtimeout: 5000 } }
)

// 读关注(Read Concern)
// "majority" — 只读取已被大多数节点确认的数据
db.orders.find().readConcern("majority")

// "local" — 默认,可能读到尚未复制到从节点的数据
db.orders.find().readConcern("local")

6.2 分片(Sharding)

当单机无法承载数据量或吞吐量时,使用分片将数据分布到多个节点。

分片架构组件

┌──────────────────────────────────────────────┐
│              mongos (路由层)                   │
│     应用程序通过 mongos 访问整个集群           │
└──────┬────────────────────────┬───────────────┘
       │                        │
┌──────▼──────┐      ┌─────────▼────────┐
│ Shard 1     │      │ Shard 2          │
│ (复制集 A)   │      │ (复制集 B)       │
│ 数据范围:    │      │ 数据范围:         │
│ 0 ~ 10000   │      │ 10001 ~ 20000    │
└─────────────┘      └──────────────────┘
        │                    │
        └────────┬───────────┘
                 │
    ┌────────────▼────────────┐
    │    Config Server        │
    │    (配置服务器,存储元数据) │
    │    必须为复制集          │
    └─────────────────────────┘

分片键选择

分片键(Shard Key)是分片的核心决策,直接影响性能:

// ======== 启用分片 ========

// 1. 连接到 mongos
mongos> sh.enableSharding("shop")

// 2. 在集合上启用分片,选择分片键
// 方式一:基于 _id 的哈希分片(最常用,数据分布均匀)
mongos> sh.shardCollection("shop.orders", { _id: "hashed" })

// 方式二:基于范围分片
mongos> sh.shardCollection("shop.orders", { customer: 1 })

// 方式三:基于复合分片键(MongoDB 5.0+)
mongos> sh.shardCollection("shop.orders", { customer: 1, date: 1 })

分片键选择原则:

原则 说明 好坏示例
高基数 分片键的值应足够多 ✅ userId ❌ gender(只有男女)
写分布均衡 写入应均匀分布到各分片 ✅ 哈希 _id ❌ 单调递增 createdAt
查询高效 尽量将查询路由到少量分片 ✅ 按 userId 查询 ❌ 按 status 查询

分片策略对比:

// 1. 哈希分片(Ranged Hash)
// 优点:写入分布最均匀
// 缺点:范围查询会广播到所有分片
sh.shardCollection("shop.logs", { _id: "hashed" })

// 2. 范围分片
// 优点:范围查询高效(只需访问相关分片)
// 缺点:数据倾斜风险(如 hot 分片)
sh.shardCollection("shop.orders", { customer: 1 })

// 3. 区域分片(Zone Sharding,按地域)
// 适用于数据本地化场景
sh.addShardToZone("shard01", "华东")
sh.addShardToZone("shard02", "华北")
sh.updateZoneKeyRange(
  "shop.orders",
  { region: "华东" },
  { region: "华东" },
  "华东"
)

分片管理命令

// 查看分片集群状态
sh.status()

// 查看数据分布
db.orders.getShardDistribution()

// 手动合并块(Chunk)
// 在分片间数据不均衡时手动触发
sh.mergeChunks(
  "shop.orders",
  { customer: "A" },
  { customer: "M" }
)

// 查看分片键范围
db.orders.getShardKey()

7. 其他 NoSQL 数据库简介

7.1 Cassandra(列族数据库)

Apache Cassandra 是 Facebook 开源的宽列存储数据库,设计哲学来自 Amazon Dynamo 和 Google Bigtable。

核心特性:

  • 去中心化架构:所有节点对等,无单点故障
  • 最终一致性:可调节一致性级别(ONE、QUORUM、ALL)
  • 线性扩展:增加节点即可线性提升吞吐量
  • 近乎零宕机:滚动升级、节点替换不影响服务

数据模型:

-- Cassandra 使用 CQL(Cassandra Query Language),类 SQL
CREATE KEYSPACE myapp
WITH replication = {
  'class': 'SimpleStrategy',
  'replication_factor': 3
};

CREATE TABLE myapp.users_by_email (
  email text PRIMARY KEY,
  name text,
  age int,
  city text
);

CREATE TABLE myapp.users_by_city (
  city text,
  email text,
  name text,
  age int,
  PRIMARY KEY (city, email)   -- 复合主键:分区键 + 聚类键
);

-- 插入
INSERT INTO myapp.users_by_email (email, name, age, city)
VALUES ('alice@example.com', 'Alice', 30, '北京');

-- 查询(必须带分区键)
SELECT * FROM myapp.users_by_city WHERE city = '北京';

适用场景:

  • IoT 设备数据采集
  • 时序指标和监控数据
  • 消息系统(如 DynamoDB 风格)
  • 需要写入吞吐量极高的场景

不适用场景:

  • 多表关联(无 JOIN 支持)
  • 事务性强的业务(无 ACID)
  • 聚合分析(不支持 GROUP BY、SUM 等)

7.2 Neo4j(图数据库)

Neo4j 是最流行的图数据库,使用属性图模型(节点+关系+属性)。

核心概念:

(Alice) —[:FOLLOWS]→ (Bob) —[:LIKES]→ (Post1)
   │                                      │
   └────—[:COMMENTED]→ (Comment1) ←————─┘

Cypher 查询语言:

// 创建节点和关系
CREATE (alice:User {name: 'Alice', age: 30})
CREATE (bob:User {name: 'Bob', age: 28})
CREATE (post:Post {title: 'MongoDB 入门教程', content: '...'})

CREATE (alice)-[:FOLLOWS]->(bob)
CREATE (bob)-[:LIKES]->(post)
CREATE (alice)-[:COMMENTED {text: '好文章!'}]->(post)

// 查询推荐的社交关系
// 找到 Bob 关注的人中 Alice 也认识的人
MATCH (alice:User {name: 'Alice'})-[:FOLLOWS]->(common:User)<-[:FOLLOWS]-(bob:User {name: 'Bob'})
RETURN common.name

// 找到 Alice 可能感兴趣的内容(好友喜欢的内容)
MATCH (alice:User {name: 'Alice'})-[:FOLLOWS]->(friend:User)
MATCH (friend)-[:LIKES]->(post:Post)
WHERE NOT (alice)-[:LIKES]->(post)
RETURN post.title, count(friend) AS recommendationScore
ORDER BY recommendationScore DESC

适用场景:

  • 社交网络(好友推荐、关系发现)
  • 反欺诈(资金流链路分析)
  • 知识图谱(实体关系检索)
  • 推荐引擎(协同过滤)
  • 权限管理(组织层级、角色继承)

7.3 InfluxDB(时序数据库)

InfluxDB 是专门为时序数据设计的数据库,由 InfluxData 开发。

核心概念:

概念 说明 示例
Measurement 类似于关系型的表 cpu_usage, sensor_temperature
Tag 索引标签(键值对) host=server01, region=cn-beijing
Field 实际指标值 value=83.5, load=0.75
Timestamp 时间戳(纳秒精度) 2024-09-15T10:00:00Z

InfluxDB 操作示例:

-- InfluxQL(类 SQL)
-- 插入时序数据
INSERT cpu_usage,host=server01,region=cn-beijing value=83.5,load=0.75 1726408800000000000

-- 查询
SELECT mean("value") AS avg_cpu
FROM "cpu_usage"
WHERE time > now() - 1h
  AND "region" = 'cn-beijing'
GROUP BY time(5m), "host"

适用场景:

  • 服务器/容器监控(CPU、内存、网络)
  • 应用性能监控(APM)
  • 物联网传感器数据
  • 金融行情数据
  • 实时仪表盘

7.4 各选型场景对比

场景 推荐数据库 原因
内容管理/博客/CMS MongoDB 文档结构灵活,支持嵌套,Schema 变化无痛
电商商品目录 MongoDB 商品属性多变,支持嵌套规格和动态字段
实时排行榜/计数器 Redis 毫秒级响应,支持 ZSET/Sorted Set
用户会话/缓存 Redis 键值快速读写,TTL 自动过期
IoT 传感器数据 Cassandra / InfluxDB 写入吞吐极高,时序聚合函数强大
社交关系推荐 Neo4j 图遍历效率比 SQL JOIN 高数个数量级
日志/审计 MongoDB / Elasticsearch 文档灵活,支持全文检索
金融核心交易 关系型数据库 需要严格 ACID 事务和审计合规
规则引擎/权限 Neo4j 树形/图结构最适合角色继承

总结

MongoDB 作为 NoSQL 文档数据库的代表,凭借其灵活的文档模型、丰富的查询语言、强大的聚合管道和原生的扩展能力,在现代 Web 应用、内容管理、大数据分析等场景中得到了广泛应用。

选择建议:

  • 如果你的数据结构频繁变化,或需要处理嵌套/不规则数据 → MongoDB
  • 如果你要求强 ACID、复杂的多表关联 → 关系型数据库
  • 如果你需要高吞吐的时序数据存储 → InfluxDB / TimescaleDB
  • 如果你需要处理复杂的关系和连接 → Neo4j
  • 如果你需要全球分布式、零宕机的系统 → Cassandra / DynamoDB

NoSQL 与关系型数据库并非对立关系,而是互为补充。现代架构中经常混合使用多种数据库,每种解决自己最擅长的问题。这正是 Persistence Polyglot(多语言持久化)的精髓。

"The right tool for the right job." — 技术选型的永恒真理。


本文所有 MongoDB 示例均在 MongoDB 8.0 中验证通过。其他数据库版本可能存在细微差异。