本节摘要:NoSQL 泛指非关系型数据库,分为键值、文档、列族、图四类;MongoDB 属于文档类。本节从一次真实的关系库迁移失败讲起,说明什么时候该选文档库、什么时候不该,并给出可落地的选型判据。
2019 年,某电商的埋点日志以 JSON 文本塞在关系库的 VARCHAR 列里。每条日志字段都在变:新版客户端加字段、灰度版本改字段名。半年内 ALTER TABLE 执行了四十多次,每次锁表都造成写入堆积,最终一次大促把库写挂了,事故定级 P1。
复盘结论只有一条:数据结构演进速度远超表结构演进速度,这就是文档数据库要解决的问题。
| 类别 | 代表 | 擅长 | 不擅长 |
|---|---|---|---|
| 键值 | Redis | 极速读写、缓存 | 复杂查询 |
| 文档 | MongoDB | 灵活结构、二级索引 | 强关系联结 |
| 列族 | Cassandra | 超大规模写入、多机房 | 任意维度查询 |
| 图 | Neo4j | 关系遍历 | 大范围聚合 |
文档数据库的核心让步:同一个集合里的文档可以有不同结构。字段加不加、嵌不嵌,由应用层决定,数据库不做强约束(可选的校验见第 5 章)。
我更倾向把这些判据写进团队规范,而不是靠个人判断:
// 埋点日志在 MongoDB 里的自然形态:结构不同也能共存 db.events.insertOne({ ts: ISODate(), userId: "u42", page: "home", os: "ios" }); db.events.insertOne({ ts: ISODate(), userId: "u43", page: "pay", channel: "alipay", amount: 99.5 });
⚠️ 选型错位是所有数据库故障里最贵的一类:它不会报错,只会让成本和复杂度慢慢失控,等发现时已经骑虎难下。
值得展开的是定位过程。大促当天写入堆积,第一反应是慢查询,DBA 拿着慢日志翻了两个小时,发现最慢的语句不是业务查询,而是一条排队中的 DDL——灰度版本的新字段又上线了,迁移脚本在等表锁。往前追溯变更记录,半年里这张埋点表被执行了四十多次 ALTER TABLE ADD COLUMN,平均每周两次,每次锁表窗口从几秒涨到几分钟,因为表已经涨到三亿行。数据量、变更频率、结构不稳定三个因素叠在一起,任何关系库都扛不住这种节奏。
修复分两步走。短期先把新字段的写入改为旁路 JSON 列,停止 ALTER;长期把整张表迁到 MongoDB,按天分集合,客户端版本号写入文档,读侧按需兼容。迁移完成后,新字段的上线从"提工单、等窗口、跑 DDL"变成"应用发版即生效",埋点表再没有因为结构变更停过机。
// 迁移后的形态:不同客户端版本的日志结构不同,天然共存 db.events.insertOne({ ts: ISODate("2024-06-01T08:00:00Z"), v: "7.2.1", userId: "u42", page: "home", os: "ios" }); db.events.insertOne({ ts: ISODate("2024-06-01T08:00:01Z"), v: "8.0.0-beta", userId: "u43", page: "pay", channel: "alipay", amount: 99.5, abGroup: "B" }); // 查询侧按需过滤,不同结构互不干扰 db.events.find({ v: /^8\.0/, abGroup: "B" }).count(); // 13742
预防措施只有一条,却最难执行:选型评审必须回答"这份数据的结构未来一年会变几次"。答不上来或者答案是"经常变",就别用需要 DDL 的存储。
选型讨论里最容易吵起来,因为双方常在不同维度上各说各话。把争论落到具体能力上会高效很多:
| 维度 | 关系库 | MongoDB |
|---|---|---|
| 结构变更 | DDL,可能锁表 | 应用层直接生效 |
| 多表联结 | JOIN,成熟优化器 | 聚合管道 lookup,代价高 |
| 事务 | 跨表强事务 | 单文档原生,多文档 4.0 起支持 |
| 校验 | 列类型、约束、外键 | 可选 JSON Schema 校验 |
| 水平扩展 | 通常靠中间件分库分表 | 原生分片,见第 7 章 |
这张表也解释了为什么"把 MongoDB 当 MySQL 用"两头不讨好:你放弃了 JOIN 和强约束,却又没利用结构灵活与水平扩展,等于只承担了代价没拿到收益。
一个具体信号清单:需求文档里出现"字段不固定""不同来源数据格式不一样""日志/轨迹/内容这种只增不改的数据";写入量级在每天千万条以上且持续增长;查询以按键取值和范围扫描为主。三条命中两条,就值得做一次认真的原型验证,用真实数据量压一压,比会议室里争论有效得多。
信号清单命中之后,别急着上会争论,先花半天做一次最小验证:把生产脱敏后的十万条真实样本灌进测试集合,用应用真实产生的五条最热查询跑一轮,记录 p99 延迟与索引内存占用;再模拟一次加字段发版,体会结构变更的顺手程度。这套动作产出的两个数字,比十页 PPT 更能终结选型争论。
// 最小验证里常被漏掉的一步:确认写入形态真的吃到了文档模型的红利 db.events.insertMany(sampleDocs); // 十万条真实样本 db.events.createIndex({ userId: 1, ts: -1 }); db.events.find({ userId: "u42" }).sort({ ts: -1 }).limit(20).explain("executionStats").executionStats.executionTimeMillis; // 若此处仍是 COLLSCAN 或扫描量巨大,说明查询形态并不适合,信号清单判错了