3052 字
约 10 分钟
22
S12-A06 · NoSQL 选型边界:文档、宽列、图与 KV

"因为要存 JSON,所以选了文档库。"这句话里,"存得进去"和"查得出来"是两件事。JSON 在 MySQL 5.7 之后也能存,也有函数索引;让关系库真正难受的不是"字段是 JSON",而是模式不稳定到无法预建索引、查询形态又还没定——而这两条恰恰说明此刻选 NoSQL 更危险。

这篇给判据与代价结构:四类模型各自的本质、能把 join 替代到什么程度、分片键选错怎么还债、以及"先关系库再迁移"和"一开始就 NoSQL"分别付什么账。


1. 需求翻译:四种常见误判

需求表述 常见误判 该问的判据
要存 JSON 所以用文档库 这些字段要按内部路径过滤、排序吗?需要跨文档一致性吗?
写入量太大 所以上宽列 写放大来自索引还是并发?单机能否承载(按写入字节/秒实测)
关系复杂 所以上图库 查询是定长路径还是变长遍历?深度可否枚举?
要极低延迟 所以上 KV 瓶颈在网络 RTT 还是存储?命中率多少?

四个误判共用一个错误:把"数据长什么样"当成了"访问路径长什么样"。选型按访问路径定,不按数据形状定——数据形状决定编码,访问路径决定引擎。


2. 关系库真正的边界

判据 关系库合适 该考虑 NoSQL
模式稳定性 字段基本确定,变更走迁移 字段按租户或来源高度异构
事务范围 多表多实体需要原子 单聚合内原子即可
查询形态 未知,需要 ad-hoc join 与聚合 固定几种访问路径,可预先建模
数据量 单机可容纳(按容量规划核对) 单机放不下且必须水平扩
团队 有 SQL 与索引经验 有对应引擎的运维经验

关系库的边界不是性能,是"模式稳定性 + 多实体事务"。绝大多数"关系库撑不住"的案例,会先被索引调整、查询改写、读写分离解决;真到单机放不下,那才轮到换引擎。


3. 四类模型的本质

类型 模型本质 天然擅长 天然不擅长
文档 聚合:一个文档是一次读写的原子单位 按主键或二级索引取整个聚合 跨文档 join、多文档事务受限
宽列 稀疏行:按 partition key 聚集,分区内按 clustering key 排序 按 key 顺序扫描、海量写入 跨分区查询、二级索引受限
图 顶点与边都是一等公民,遍历是原生操作 变长路径、邻接展开 全量扫描聚合、超级节点
KV 纯寻址,值对引擎不透明 极低延迟单键读写、缓存 任何基于值内容的查询

一句话概括宽列与 KV 的差别:宽列是"有序的 KV"。只有分区内有序,范围扫描与"取最近 N 条"才成立;KV 没有这一层,需要顺序就得在应用里维护。


4. 查询能力对照

能力 文档 宽列 图 KV 关系库
主键取 是 是 是 是 是
二级索引 有(显式建,写放大) 受限(物化视图/SAI) 属性索引 无,需外部索引 是
范围扫描 索引上可以 分区内天然有序 邻接上有序 无 是
聚合 聚合管道 需预聚合表 局部 无 是
join $lookup(代价高) 应用侧拼装 遍历即 join 应用侧拼装 原生
排序分页 索引或内存排序 需按排序键建模 局部 无 是

判据:表里写着"无"的那几格,就是未来要写进应用层的代码。评审时把这些"无"折算成工作量与维护面,比争论基准测试数字有用得多——数字随版本变化,缺的能力不会自己长出来。


5. 一致性:按操作分级,而不是按库定

模型 语义 典型实现 业务判据
强一致(线性化) 读到的必是最新已提交 单主复制;Quorum 且 R+W>N 余额、库存、唯一约束
最终一致 一段时间后收敛 异步复制、gossip 信息流、计数、搜索索引
读己之写 同一会话读到自己刚写的 会话粘滞或读主 提交后立刻回显
单调读 不会读到更旧的版本 版本号过滤、粘滞 分页浏览

多数"业务不能接受最终一致"其实是"某个具体操作不能"。拆开之后往往只剩两三个接口需要强一致,成本比整库选强一致低一个数量级。

# 一致性与事务一般按操作可调,不必全局设定(按各引擎当前版本文档核对选项名)
MongoDB  : insertOne(doc, {writeConcern:{w:"majority", j:true}})
           find(q).readConcern("majority").readPref("primary")
Cassandra: SELECT ... WHERE user_id = ?   -- CONSISTENCY LOCAL_QUORUM
DynamoDB : GetItem(..., ConsistentRead = true)

6. 分片键:一次选错,长期还债

选错的形态 症状 修复代价
单调递增(时间戳、自增) 写全部打到末端分片 换键 + 全量重分布
低基数字段(状态、地区) 分片倾斜、热点分片 加盐或复合键,所有查询跟着改
与查询不对齐的高基数字段 每次查询 scatter-gather 改模型或建物化查询表
会变化的字段 更新即搬移,产生墓碑 改不可变键 + 应用侧重构
-- 宽列:partition key 决定落点,clustering key 决定分区内顺序
CREATE TABLE events (
  tenant_id uuid, bucket date, event_id timeuuid, payload text,
  PRIMARY KEY ((tenant_id, bucket), event_id)
) WITH CLUSTERING ORDER BY (event_id DESC);

-- 查询必须带完整 partition key,否则就是跨分区扫描

规则一句:查询必须先落到一个分区,模型是围绕这句话反推出来的。bucket 列不是为了美观,它是把单分区体积压住的唯一手段——单分区无限增长是宽列库最常见的长期事故。


7. 两条路线与混合架构的同步

阶段 先关系库再迁移 一开始就 NoSQL
前期 快,SQL 表达力完整 建模与访问路径必须先定死
中期 模式不稳时改表、加索引 访问路径一变就要改模型 + 回填
转折点 双写 + 校验 + 切换,按月计 无迁移,但早期建模错要重来
失败模式 迁移半途,两套系统长期并存 模型错,所有查询一起退化

混合架构不是"三选一"的反面,它把同步问题显性化了:

组合 同步方向 风险
关系库主数据 + KV 缓存 写后失效,或先失效后写 脏读窗口
关系库 + 搜索库 CDC/binlog,或应用双写 索引落后、事件乱序
关系库 + 图库 关系变更事件 图滞后,遍历结果缺边
-- outbox:与业务写在同一事务内落一条事件,再由 CDC 投递
BEGIN;
INSERT INTO orders (id, user_id, amount) VALUES (?, ?, ?);
INSERT INTO outbox (id, aggregate, payload, created_at) VALUES (?, 'order', ?, now());
COMMIT;
# 消费端幂等:按事件 id 去重,再写目标库
if seen(event.id): return
target.upsert(key=event.aggregate_id, doc=event.payload)
seen.put(event.id, ttl=7d)

两条纪律:同步只走"事件 + 幂等",不走"双写 + 重试";目标库允许落后,但必须能从源库重放重建,做不到就不算设计完成。


8. 运维与迁移

维度 关系库 文档 宽列 图
备份 逻辑 + 物理,工具成熟 mongodump 或快照 快照(修复代价高) 逻辑导出
恢复演练 常规操作 需专门演练 节点重建慢,需演练 少有人做
扩容 纵向为主 分片 + 均衡 加节点即扩,需重平衡 随图规模增长
监控要点 索引、慢查询 分片均衡、连接数 compaction、墓碑、GC 停顿 遍历深度
阶段1  双写:旧库为准,新库旁路,只观察差异
阶段2  按比例灰度读,保留回退开关
阶段3  新库为准,旧库只读,跑全量校验
阶段4  停双写,保留快照 N 天后下线
迁移阶段 通过判据
双写 差异率稳定在抽样噪声内,且每一条差异可解释
灰度读 错误率与 P99 不劣于同期基线
以新为准 抽样字段一致,业务侧核对无异常
下线旧库 回滚演练过,快照可恢复

9. 排错:五种"能写进去但查不出来"

现象 原因 处理
写入延迟随数据量线性上升 分片键单调,或单分区过大 换键、加分桶,检查分区体积
查询偶发超时,重试就好 跨分片 scatter-gather 的尾部延迟 补全 partition key,或建物化表
计数类结果不准 最终一致下的读 按操作分级一致性,关键读走主/quorum
索引建了但查询没走 索引前缀与查询字段顺序不匹配 按查询反推索引顺序
迁移后磁盘暴涨 墓碑累积、未触发 compaction 手动 compaction,检查 TTL 与删除模式

10. 边界:什么时候不该选 NoSQL

场景 结论
数据量在单机能力内(按容量规划核对) 关系库 + 索引,不引入新引擎
查询形态还在变 关系库;访问路径稳定后再谈迁移
团队没有该引擎的运维经验 不选;运维成本会盖过建模收益
只需要缓存语义 KV 做缓存,不当主库
需要多实体强事务 关系库,或明确接受受限事务语义
关系深度可枚举且只有少数几跳 关系库 join 足够,图库收益要能量化

收束:NoSQL 买到的是"按已知访问路径水平扩展",付出的是"查询自由"与"运维新技能"。 访问路径没定下来之前,这笔交易是亏的。


11. 自测

  1. "要存 JSON"为什么不足以成为选文档库的理由?
  2. 判断"关系库是否到边界"的两个核心判据是什么?
  3. 宽列与 KV 的模型差别,为什么可以概括为"有序的 KV"?
  4. 查询能力表里出现"无"的那几格,应该折算成什么成本?
  5. 分片键选了单调递增字段,为什么修复代价不是"改个配置"?
  6. 混合架构里,为什么"目标库能从源库重放重建"比"同步实时性"更该先确认?
S12-A06 · NoSQL 选型边界:文档、宽列、图与 KV
https://www.clxhxhhr.top/posts/6149/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。