15.1 从访问模式选择 NoSQL 数据模型
关系模型不是“旧技术”,NoSQL 也不是一个具有统一能力的产品类别。所谓 NoSQL,只是把键值、文档、宽列、图等差异极大的系统放在一个历史标签下。选型时应比较数据模型、访问路径和一致性边界,而不是比较两个阵营。
先问关系数据库是否真的不够用
在更换数据库前,先确认问题属于哪一层:
- 查询慢,是否缺索引、统计信息或正确 SQL?
- 写入热点,是否能分区、批处理或重新设计键?
- JSON 字段变化频繁,关系数据库的 JSON 类型和在线 DDL 是否已经够用?
- 图遍历是核心工作负载,还是偶尔的一条递归查询?
- 需要全球多活,还是只需要同城高可用和只读副本?
现代关系数据库同样支持分区、复制、JSON、全文索引和横向扩展方案。NoSQL 的价值通常不是“关系数据库做不到”,而是某个系统把一类访问模式做成了更直接的接口和更可预测的扩展单元。
从访问模式反推数据模型
建模前先列出高频操作:
操作名称 输入键 返回范围 写入原子边界
读取玩家档案 player_id 单个聚合 单文档
读取时间线 user_id + time 最近 100 条 单分区
查最短关系链 start_id, end_id 多跳路径 多节点/边
读取配置 config_key 单值 单键如果不知道查询从什么键开始、一次需要多少记录、哪些字段必须一起提交,就还不能选择数据库。
键值模型:能力集中在主键路径
键值存储把主要接口收敛到 key -> value。但“值是不透明字节”只适用于某些产品;Redis 的值是可操作数据结构,DynamoDB 还支持复合主键、二级索引和文档属性。
适合的场景通常具备:
- 请求天然带完整主键;
- 单键或明确事务范围内的操作占绝大多数;
- 可以通过键设计分散热点;
- 不依赖临时增加的任意过滤条件。
键设计同时决定数据局部性和负载分布。例如:
PK = tenant#42
SK = order#2026-08-03T10:15:00Z#9182这里是假设 DynamoDB 复合键的形状;查询提供 PK 等值条件和适当的 SK 范围,并保持时间字符串使用统一 UTC 格式和精度,字符串次序才对应时间次序。
它能按租户和时间范围读取订单,但若某个租户特别热门,仍可能形成热点。哈希分布不等于业务负载自动均匀。
文档模型:把共同读取和更新的数据聚在一起
文档数据库用对象或 JSON/BSON 类结构保存嵌套数据。核心设计问题是嵌入还是引用:
- 一起读取、生命周期一致、数量有界的子对象适合嵌入;
- 独立增长、被多处共享或可能无限扩大的集合更适合引用或拆表;
- 文档原子性不能保护未纳入同一文档的业务不变量。
这是 JavaScript 对象形状示例,尚未调用 MongoDB 驱动;金额为示意数值,实际接口需声明货币与计量单位。
const order = {
_id: "order-9182",
customer_id: "customer-7",
status: "paid",
lines: [
{ sku: "product-1", quantity: 1, unit_price: 80 }
],
total: 80
};这里把订单行嵌入订单,是因为读取和生命周期一致;商品当前价格不应只靠这个副本维护,它是下单时的价格快照。
“灵活 Schema”也不等于没有 Schema。字段含义、类型、版本迁移、索引和校验仍然存在,只是更多责任转移到应用和部署流程。MongoDB 等产品还提供 schema validation 和多文档事务,不能再用“文档库没有约束和事务”概括。
宽列模型:围绕分区键预计算查询形状
Cassandra 的 wide-column 模型不是分析数据库的“列式存储”。它把数据按 partition key 分布,同一分区内再按 clustering columns 排序。
下面是 Cassandra CQL(适用于 4.x/5.x)的建表片段,需先选中练习 keyspace;不能当成关系数据库 SQL 执行。
CREATE TABLE events_by_device (
device_id text,
event_day date,
event_time timestamp,
event_id timeuuid,
payload text,
PRIMARY KEY ((device_id, event_day), event_time, event_id)
) WITH CLUSTERING ORDER BY (event_time DESC);这个表明确服务“读取某设备某天的时间序事件”。若还要按告警类型查询,通常要为那个访问模式建立另一张反规范化表,而不是期待任意 WHERE。
分区必须有界:只有 device_id 可能让多年事件堆进一个巨大分区,因此加入 event_day 做 bucket。代价是跨天读取需要由客户端或服务层合并。每天一个 bucket 也不是容量保证:假设某设备每秒 1000 条,一天仍有 8640 万条,应根据记录宽度和目标分区大小缩短时间桶或增加可控 shard。
图模型:关系本身是一等数据
属性图通常由节点、带方向和类型的关系,以及两者的属性构成:
下面是 Neo4j Cypher 的查询片段,需已有 User 节点、FOLLOWS 关系和绑定的 from/to 参数;User.id 应有唯一性约束或合适索引。
MATCH path = (a:User {id: $from})-[:FOLLOWS*1..4]->(b:User {id: $to})
RETURN path
LIMIT 1;这段只返回 1–4 跳中的任意一条匹配路径,LIMIT 1 不保证最短路径,也不限制搜索过程只访问一个节点。若需最短路径,应使用所选 Cypher 版本的 shortest-path 语法并检查执行计划。Neo4j 可变长度路径
图数据库适合多跳遍历、路径、连通性和模式匹配是主要负载的场景,例如权限继承、欺诈网络和知识图谱。若业务只是按外键连接一两层,关系数据库往往更简单。
图模型也需要约束、索引和事务;“没有 JOIN”不等于没有遍历成本。高出度超级节点、无界路径和错误方向仍会造成搜索爆炸。
选型不是一张产品排行榜
| 主要需求 | 优先评估 | 关键风险 |
|---|---|---|
| 强约束、多种临时查询、跨实体事务 | 关系数据库 | 扩展边界、热点与复杂查询治理 |
| 完整主键访问、可预测低延迟 | 键值系统 | 键设计、热点、二级访问路径 |
| 聚合对象读写、嵌套结构 | 文档数据库 | 文档无界增长、重复数据一致性 |
| 超大规模按分区顺序访问 | 宽列系统 | 查询驱动建模、墓碑、repair 与热点 |
| 多跳关系与路径查询 | 图数据库 | 超级节点、遍历上界、集群扩展 |
一套业务可以使用多种存储,但每增加一种就增加备份、监控、权限、升级、CDC 和故障处理成本。先让一个权威数据库承担事务事实,再用缓存、搜索或图投影服务特殊读模型,通常比“每类数据都配专门数据库”更稳妥。
本节检查点
- 能从访问模式和原子边界反推键与数据模型。
- 能区分宽列模型与 OLAP 列式存储。
- 不再用“无 Schema、无事务、不能 JOIN”概括全部 NoSQL。
- 能说明引入第二种数据库后新增的同步与运维成本。
参考
下一节讨论分布式存储真正绕不开的故障条件:网络分区发生时,一次读写究竟选择拒绝、等待还是返回可能陈旧的结果。