MongoDBNotes

第 29 章:项目实战

zjc 于 2026-01-29 发布

这是《MongoDB 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 本章设计一个“商品与订单中心”的 MongoDB 方案,覆盖 Schema、索引、CRUD、状态机、聚合、Change Stream、分片和监控。示例以学习为主,生产地址和密钥需按环境替换。

29.1 业务需求

核心用例:

  1. 按分类和关键词查询商品;
  2. 查看商品详情;
  3. 用户查看订单列表;
  4. 订单支付状态流转;
  5. 统计商品销量;
  6. 同步商品到搜索系统;
  7. 未来支持租户扩展。

数据特点:

  1. 商品读取多、更新中频;
  2. 订单按用户增长;
  3. 商品详情较大;
  4. 订单明细有限;
  5. 搜索同步要求增量。

29.2 集合设计

商品主表:

{
  _id: "sku_10001",
  tenant_id: "t_100",
  title: "轻薄笔记本",
  category_id: "c_notebook",
  price: NumberDecimal("5999.00"),
  status: "ON_SALE",
  summary: "16 核 32GB",
  keywords: ["轻薄", "笔记本"],
  attrs: {
    cpu: "16核",
    memory: "32GB"
  },
  sales_count: 1200,
  version: 3,
  created_at: new Date(),
  updated_at: new Date()
}

商品详情表:

{
  _id: "sku_10001",
  content: "<html>...</html>",
  images: ["a.jpg", "b.jpg"]
}

订单表:

{
  _id: "o_202608250001",
  tenant_id: "t_100",
  user_id: "u_1001",
  status: "WAIT_PAY",
  total_amount: NumberDecimal("5999.00"),
  item_count: 1,
  items: [
    {
      sku_id: "sku_10001",
      title: "轻薄笔记本",
      price: NumberDecimal("5999.00"),
      quantity: 1
    }
  ],
  shipping_address: {
    city: "Shanghai",
    street: "南京西路 100 号"
  },
  version: 1,
  created_at: new Date(),
  updated_at: new Date()
}

29.3 索引设计

商品:

db.products.createIndex({ tenant_id: 1, category_id: 1, status: 1, sales_count: -1 })
db.products.createIndex({ tenant_id: 1, keywords: 1 })
db.products.createIndex({ tenant_id: 1, title: 1 })

订单:

db.orders.createIndex({ tenant_id: 1, user_id: 1, created_at: -1, _id: -1 })
db.orders.createIndex({ tenant_id: 1, status: 1, created_at: -1 })

设计说明:

  1. 查询都带租户前缀;
  2. 列表排序进入索引;
  3. _id 保证分页边界唯一;
  4. 避免无关字段进入索引;
  5. 写入热点可后期评估分片。

29.4 创建订单

function createOrder(order) {
  const result = db.orders.insertOne({
    _id: order.orderId,
    tenant_id: order.tenantId,
    user_id: order.userId,
    status: "CREATED",
    total_amount: order.totalAmount,
    item_count: order.items.length,
    items: order.items,
    shipping_address: order.address,
    version: 1,
    created_at: new Date(),
    updated_at: new Date()
  }, {
    writeConcern: { w: "majority", j: true, wtimeout: 3000 }
  });

  return result;
}

订单号唯一,重复提交由 _id 唯一约束拦截。

29.5 支付状态机

function markPaid(orderId, version) {
  const result = db.orders.updateOne(
    {
      _id: orderId,
      status: "WAIT_PAY",
      version: version
    },
    {
      $set: {
        status: "PAID",
        paid_at: new Date()
      },
      $inc: { version: 1 },
      $currentDate: { updated_at: true }
    }
  );

  if (result.matchedCount === 0) {
    throw new Error("order state changed");
  }

  return result;
}

状态机条件防止重复回调、旧请求和并发覆盖。

29.6 用户订单分页

第一页:

db.orders.find(
  {
    tenant_id: "t_100",
    user_id: "u_1001"
  },
  {
    status: 1,
    total_amount: 1,
    item_count: 1,
    created_at: 1
  }
).sort({ created_at: -1, _id: -1 }).limit(20)

下一页:

db.orders.find({
  tenant_id: "t_100",
  user_id: "u_1001",
  $or: [
    { created_at: { $lt: lastCreatedAt } },
    { created_at: lastCreatedAt, _id: { $lt: lastId } }
  ]
}).sort({ created_at: -1, _id: -1 }).limit(20)

29.7 商品销量统计

db.orders.aggregate([
  {
    $match: {
      tenant_id: "t_100",
      status: "PAID",
      created_at: {
        $gte: new Date("2026-08-01T00:00:00Z"),
        $lt: new Date("2026-09-01T00:00:00Z")
      }
    }
  },
  { $unwind: "$items" },
  {
    $group: {
      _id: "$items.sku_id",
      quantity: { $sum: "$items.quantity" },
      amount: { $sum: "$items.price" },
      order_count: { $sum: 1 }
    }
  },
  { $sort: { quantity: -1 } },
  { $limit: 50 }
])

重统计建议移到 Hidden Secondary 或分析库。

29.8 搜索同步

全量初始化:

db.products.find({
  tenant_id: "t_100",
  status: "ON_SALE"
}, {
  title: 1,
  keywords: 1,
  category_id: 1,
  price: 1
})

增量监听:

const stream = db.products.watch(
  [
    {
      $match: {
        operationType: { $in: ["insert", "update", "replace", "delete"] }
      }
    }
  ],
  {
    fullDocument: "updateLookup"
  }
)

同步程序保存 resume token,并对下游搜索文档执行幂等 upsert。

29.9 分片方案

当订单规模超过单 shard 容量:

sh.enableSharding("shop")
sh.shardCollection(
  "shop.orders",
  { tenant_id: 1, user_id: 1 }
)

理由:

  1. 高频查询带 tenant_iduser_id
  2. 租户内部用户基数高;
  3. 写入按租户和用户分布;
  4. 每个租户事务多落在同一 shard;
  5. 大租户需要单独监控。

29.10 监控与验收

指标:

product_query_latency_p99
order_create_latency_p99
order_state_conflict_total
order_page_query_p99
search_sync_lag_seconds
change_stream_resume_errors
disk_usage
cache_hit_ratio
replication_lag

验收用例:

  1. 重复订单号不会重复创建;
  2. 重复支付回调只生效一次;
  3. 订单分页不重不漏;
  4. 商品列表查询走索引;
  5. 详情读取不影响列表延迟;
  6. 搜索同步可恢复;
  7. 分页查询带租户前缀;
  8. 副本切换后应用可恢复。

本章小结

项目实践把文档边界、索引、状态机、游标分页、聚合、Change Stream 和分片键串联起来。商品与订单中心的关键是列表轻量化、详情拆分、状态条件更新和搜索增量同步。

思考题

  1. 为什么商品详情要拆集合?
  2. 订单状态机为什么需要版本?
  3. 分页为什么要 created_at + _id
  4. 搜索同步如何处理 token 过期?
  5. 订单分片键为什么选择租户和用户?