NoSQL("Not Only SQL")是一大类数据库系统,放弃关系表模型以换取灵活 schema、横向扩展和为特定访问模式调优的数据模型。它们诞生于 web 规模公司在海量读写吞吐和快速演化的 schema 下撞上关系数据库的极限之时。Google 2006 年发表 Bigtable,Amazon 2007 年发表 Dynamo,一个行业由此诞生。今天 NoSQL 涵盖四种根本不同的数据模型——键值、文档、列族、图——各解决一类不同问题。理解哪个模型适合哪种负载、以及为什么,是区分高级工程师的存储决策与初级工程师抛硬币的所在。

⚡ 速览要点
  • 键值(Key-value)——按精确 key 做 O(1) 读写;无 schema、无查询;适合会话、缓存、限流。
  • 文档(Document)——自描述 JSON/BSON;灵活 schema、无 join 的嵌套对象;适合用户资料、目录。
  • 列族(Column-family)——行按 row key 索引,列按 family 分组并存;适合时序和高写分析负载。
  • 图(Graph)——节点和边的原生遍历;适合社交图、推荐、欺诈检测。
  • CAP 定理——分区容错强制必需;真正的选择是 CP(HBase、ZooKeeper)vs AP(Cassandra、最终模式的 DynamoDB)。
  • 默认用 SQL;只在有专门数据模型能解决的特定瓶颈时才动用 NoSQL。
tldr

NoSQL 数据库用关系特性(join、严格 ACID)换 schema 灵活性和横向扩展。为你的数据模型选对类型:嵌套对象用文档,高吞吐查找用键值,时序和分析用列族,关系遍历用图。CAP 定理规定网络分区期间你必须在一致性和可用性间选择。

NoSQL 数据库分类与数据模型
NoSQL 数据库分类与数据模型

四大 NoSQL 类别

键值存储——速度层

最简单的数据模型:每个值存在一个唯一 key 下。按 key 读写是 O(1)——字面上一次哈希表查找。没有查询语言、没有 schema、没有关系——只有精确查找的纯速度。值本身对存储完全不透明;它可以是字符串、序列化对象、二进制 blob 或计数器。数据库不解释它。

Redis 值得特别关注,因为它远超简单 KV 存储。它的数据结构——字符串、列表、集合、有序集合、哈希、流、HyperLogLog——让你实现精巧的模式,如滑动窗口限流(对时间戳 ZRANGEBYSCORE)、pub/sub 扇出、排行榜(ZADD/ZRANK),以及通过 SET key value NX PX ttl 原子条件写实现的分布式锁。关键洞见是 Redis 在单线程事件循环里原子地执行所有操作,消除了困扰多线程进程内缓存的并发 bug。

redis-cli
# 带 TTL 的会话存储
SET session:abc123 '{"userId":42,"role":"admin"}' EX 3600

# 限流器:每用户 100 req/分钟的滑动窗口
ZADD ratelimit:user42 1716550000.123 "req:a1b2"
ZREMRANGEBYSCORE ratelimit:user42 -inf 1716549940.000  # 修剪旧条目
ZCARD ratelimit:user42                                    # 计数窗口内剩余

# 分布式锁——原子 compare-and-set
SET lock:order-42 "owner-xyz" NX PX 30000  # 仅当 key 不存在时成功

文档存储——schema 灵活层

数据存为自描述文档(通常 JSON 或 BSON)。一个文档可包含嵌套对象和数组,无需 join 就表示一个完整实体。schema 灵活——同一集合的不同文档可有不同字段。这不是弱点;它是刻意的设计选择。当你的数据模型真正异构(一个商品目录里笔记本有 CPU/RAM 规格、T 恤有尺码/颜色变体)时,把一切塞进规范化关系 schema 要么产生一片可空列森林,要么产生可怕的 entity-attribute-value 反模式。

javascript
// MongoDB:插入并查询一个文档
await db.collection('users').insertOne({
  _id: 'u-123',
  name: 'Alice',
  email: 'alice@example.com',
  address: { city: 'Seattle', zip: '98101' },  // 嵌套对象,无需 join
  tags: ['admin', 'beta']
});

const user = await db.collection('users').findOne(
  { 'address.city': 'Seattle' }   // 查询嵌套字段
);

// DynamoDB:单表设计模式——所有实体类型在一张表
// PK = "USER#alice"   SK = "PROFILE"           → 用户记录
// PK = "USER#alice"   SK = "ORDER#2024-01-15"  → 订单记录
// 支持单次查询取出 用户 + 近期订单
const result = await ddb.query({
  TableName: 'AppTable',
  KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
  ExpressionAttributeValues: { ':pk': 'USER#alice', ':prefix': 'ORDER#' }
});

DynamoDB 在这里值得一提。尽管被当作 KV 存储营销,它的复合键模型(分区键 + 排序键)配 GSI 和 LSI 使它成为面向访问模式设计的强大文档存储。单表设计模式——把所有实体类型存在一张表、通过排序键前缀共置相关记录——让你在单次查询里满足多个访问模式,以前期建模严格性为代价完全消除 join。

列族存储——写扩展层

数据组织成由 row key 标识的行,列分组进列族(column family)。同一 family 内的列在磁盘上一起存储,使读大量行的一小组列极其高效——经典的分析访问模式。行可有不同列,新列无需 schema 迁移就能添加。关键设计洞见:主键(在 Cassandra 里是分区键 + 聚簇键)完全决定数据存在哪、哪些查询高效。你围绕查询模式而非规范化实体设计表。

cql
-- Cassandra:时序传感器读数
-- 分区键: sensor_id(一个分区 = 一个传感器的数据)
-- 聚簇键: ts DESC(分区内最新优先)
CREATE TABLE sensor_readings (
  sensor_id   UUID,
  ts          TIMESTAMP,
  value       DOUBLE,
  unit        TEXT,
  PRIMARY KEY (sensor_id, ts)
) WITH CLUSTERING ORDER BY (ts DESC);

-- 取一个传感器最近 100 条读数:单分区,无 scatter
SELECT * FROM sensor_readings
WHERE sensor_id = 550e8400-e29b-41d4-a716-446655440000
LIMIT 100;

-- Cassandra 写路径:写进 commit log + memtable
-- 然后周期性刷到磁盘上不可变的 SSTable
-- → 无论数据量多大都是 O(1) 写

Cassandra 的写路径解释了它为何能维持每秒数百万写:每次写追加到内存 memtable 和顺序 commit log,然后 memtable 周期性刷到不可变 SSTable。没有原地更新、没有锁、写时无索引维护。代价在 compaction 时付。这种 LSM-tree 架构(Log-Structured Merge-tree)也被 RocksDB、LevelDB、HBase 使用——它是列族存储主导写多负载的根本原因。

图数据库——关系层

数据建模为节点(实体)和边(关系),两者都有属性。图遍历——"找住在纽约、也买了商品 X 的朋友的朋友"——是原生一等操作,无需昂贵的多级 join 就能实现。关系数据库里同样的查询需要在好友表上多次自连接,性能随深度多项式退化。图数据库里,每个节点直接存指向其相邻边的指针——遍历是每跳 O(度),与图总大小无关。

cypher
// Neo4j Cypher:找尚非好友的二度连接
MATCH (me:Person {id: 'alice'})-[:FRIENDS_WITH]->(friend)
                                 -[:FRIENDS_WITH]->(fof)
WHERE fof.id <> 'alice'
  AND NOT (me)-[:FRIENDS_WITH]->(fof)
RETURN fof.name, count(*) AS mutual_friends
ORDER BY mutual_friends DESC
LIMIT 10;

// 欺诈检测:找账号间共享的标识符
MATCH (a:Account)-[:USES]->(device:Device)<-[:USES]-(b:Account)
WHERE a.id <> b.id
RETURN a.id, b.id, device.fingerprint AS shared_device

CAP 定理——分布式系统约束

CAP 定理由 Eric Brewer 提出、Gilbert 和 Lynch 形式化,陈述一个分布式数据存储只能同时保证以下三个属性中的两个:

属性含义
一致性 (C)每次读返回最新的写或一个错误。所有节点同时看到相同数据。这是线性一致性——一个强安全保证。
可用性 (A)每个请求收到非错误响应,即便它可能不是最新数据。系统从不拒绝服务。
分区容错 (P)即便网络分区阻止某些节点通信,系统仍继续运作。节点间消息可能丢失或延迟。

网络分区在任何分布式系统中不可避免——交换机故障、机架掉电、跨数据中心链路退化。所以 P 实际上是强制的。真正的选择在 CP(分区期间牺牲可用性——返回错误而非陈旧数据;HBase、ZooKeeper)和 AP(牺牲一致性——服务陈旧数据而非报错;Cassandra、最终一致模式的 DynamoDB)之间。大多数 NoSQL 数据库让你通过一致性级别按操作调这个。

PACELC——更完整的图景

CAP 只描述网络分区期间的行为。PACELC 扩展它:即便没有分区,延迟和一致性之间仍有取舍。DynamoDB 的"最终一致读"更快,因为它们能从任何副本服务;"强一致读"必须路由到主并等它确认没有更新的写存在。真实生产系统在 PACELC 轴上调优的频率远高于 CAP 轴,因为分区罕见但延迟/一致性取舍持续存在。

深入一致性模型

CAP 二元的"C 或 A"框架对生产决策太粗。真实系统提供一个一致性保证谱系,理解它们让你按操作而非按数据库选对级别:

cql
-- 3 副本集群的 Cassandra 一致性级别
-- (RF=3,所以 QUORUM = 必须 2 个副本响应)

-- 强一致:W=QUORUM + R=QUORUM 保证无陈旧读
INSERT INTO orders (id, status) VALUES (42, 'PAID') USING CONSISTENCY QUORUM;
SELECT * FROM orders WHERE id = 42 CONSISTENCY QUORUM;

-- 最大吞吐:W=ANY + R=ONE — 最快但最陈旧
INSERT INTO page_views (url, ts) VALUES ('/home', now()) USING CONSISTENCY ANY;
SELECT count FROM page_view_totals WHERE url = '/home' CONSISTENCY ONE;

BASE vs. ACID——正确性取舍

关系数据库由 ACID 保证定义。NoSQL 数据库通常转而提供 BASE 语义。理解这个取舍是选存储层的基础:

属性ACID (SQL)BASE (NoSQL)
原子性事务里所有操作全成或全败——无部分状态单实体操作原子;多实体操作可能留下可见的部分状态
一致性数据库从一个有效状态移到另一个;约束被强制基本可用(Basically Available)——系统即便有故障也推进
隔离性并发事务看似串行执行软状态(Soft state)——数据可能无显式写就随时间改变(TTL 过期、compaction)
持久性已提交数据挺过崩溃(WAL/journal)最终一致(Eventually Consistent)——副本收敛而无需同步达成一致

实际后果:对任何部分写会造成语义错误的领域用 SQL 和 ACID——金融账本、库存计数、订单记录。对临时不一致可接受的领域用 BASE 语义——社交 feed、读缓存、分析计数器、推荐分数。工程师犯的错误是假设所有数据都落入一类。在真实系统里,你几乎总是两者都需要:一个关系数据库存事务真相,一个或多个 NoSQL 存储处理特定高吞吐或高规模负载。

数据建模方法

文档存储建模——嵌入 vs 引用

文档存储的核心设计决策是把相关数据嵌入文档内,还是用外键等价物引用它。经验法则遵循"one-to-squiggly"准则:若相关实体基数有界且总与父一起访问,嵌入它。若它基数无界或有时被独立访问,引用它。

反模式:嵌入无界增长的数组。一个嵌入它所有评论的订单文档最终会撞上 MongoDB 16 MB 文档大小限制,并拖慢该订单的每次读取,即便不需要评论。通过问"这个数组永远增长吗?"来检测它。若是,改用引用。

列族建模——围绕查询设计表

在 Cassandra 里你不先规范化数据再写查询。你先写查询,再设计表来恰好服务那些查询。每张表是为一个特定访问模式优化的、数据的反规范化投影。"用户的活动 feed"和"所有 X 类型的活动"在 Cassandra 里需要两张独立的表,两者在每次写时都填充——一个写放大取舍,买到已知模式上的 O(1) 读。

cql
-- 查询 1:"给我用户 alice 的 feed,最新优先"
CREATE TABLE user_feed_by_user (
  user_id     UUID,
  created_at  TIMESTAMP,
  activity_id UUID,
  type        TEXT,
  payload     TEXT,
  PRIMARY KEY (user_id, created_at, activity_id)
) WITH CLUSTERING ORDER BY (created_at DESC);

-- 查询 2:"全局显示所有 'purchase' 事件,最新优先"
CREATE TABLE activities_by_type (
  type        TEXT,
  created_at  TIMESTAMP,
  user_id     UUID,
  payload     TEXT,
  PRIMARY KEY (type, created_at)
) WITH CLUSTERING ORDER BY (created_at DESC);

-- 写时:在应用里填充两张表

代表性产品深入

DynamoDB——不计代价的托管扩展

DynamoDB 是 AWS 全托管、serverless 的 NoSQL 数据库。其定义性属性:任意规模个位数毫秒延迟、自动分片,以及基于预置或按需读/写容量单位的定价模型。其设计哲学——对访问模式高度强观点——强制前期建模纪律,在规模上回报丰厚。全局表提供带冲突解决的多区域 active-active 复制。DynamoDB Streams 能喂 CDC 管道。取舍:没有多表和应用侧 join 就无法做复杂查询;二级索引(GSI)是最终一致;跨分区事务存在(通过 TransactWriteItems)但昂贵且限于 100 项。

MongoDB——文档通才

MongoDB 是部署最广的文档数据库。它的聚合管道——一连串阶段,包括 $match$group$lookup(服务端 join)、$unwind$project——覆盖了纯 KV 存储里需要多次查询的分析用例。Change Streams 支持 CDC 风格的管道。Atlas Search 在文档集合之上提供 Lucene 驱动的全文搜索。取舍:无 schema 集合若不通过应用级校验或 JSON Schema 约束施加纪律,会变成"无 schema 混乱";多文档 ACID 事务可用但应是例外而非规则;横向分片需要显式分片键规划,运维复杂。

Cassandra——写扩展冠军

Cassandra 由 Facebook 构建以支撑收件箱搜索功能,然后开源并被 Netflix、Apple 和 Discord(用它存数十亿消息)采用。它无主、点对点的架构(无主节点、无单点故障)意味着它能吸收节点故障甚至数据中心宕机而无停机。一致性哈希环把分区分散到节点,虚拟节点(vnode)概念把每个物理节点的 token 范围铺开,即便硬件异构也确保均匀分布。Discord 著名的"Cassandra 迁移"帖子记录了他们如何从 Cassandra 迁到 ScyllaDB(一个性能更好的无 JVM C++ 重实现)——值得一读,了解规模化列族存储的运维现实。

Neo4j——图标准

Neo4j 使用原生图存储格式,每个节点直接存指向其相邻边的指针,消除关系数据库做 join 所需的索引查找。它的 Cypher 查询语言对关系密集的查询可以说比 SQL 更可读。实际天花板:Neo4j 社区版是单节点;企业集群模型在数百亿节点和边以内工作良好,但跨机器分片一个属性图仍是个难研究问题。对真正巨大的图(Facebook 社交图),用自研方案或像 JanusGraph(在 Cassandra/HBase 之上)这样的专建系统。

CAP 实践:NoSQL 产品定位

数据库类型CAP 定位默认一致性
Redis (cluster)KVCP — 主持有写;副本可能滞后主上强;副本异步
DynamoDBKV / 文档AP(默认)/ CP(强读)最终;可选强
MongoDB文档CP(主选举)默认主读;可调
Cassandra列族AP(默认)/ CP(QUORUM)最终;按操作可调
HBase列族CP — region server 故障时写阻塞强(单个 region server 拥有范围)
Neo4jCP(单主写)主上强;读副本异步

NoSQL vs. SQL:何时选哪个

场景倾向 SQL倾向 NoSQL
Schema稳定、定义良好的关系快速演化或每实体高度可变
事务需要多表 ACID(金融、订单)单实体更新足够;BASE 可接受
查询模式跨实体的灵活、临时 SQL 查询前期设计的已知、重复访问模式
规模中等——垂直扩展 + 读副本海量——需横向写扩展(百万/秒)
一致性处处需要强一致大多数读可接受最终一致
数据形状表格、规范化、关系型层级、类图、或时序

何时不该用 NoSQL

围绕 NoSQL 的炒作周期让许多团队过早放弃 SQL。以下是 NoSQL 错误选择的场景:

多语言持久化的现实

规模化的生产系统几乎从不用单一数据库类型。典型架构:PostgreSQL 做事务核心(订单、账户、用户),MongoDB 做灵活目录和内容,Cassandra 或 DynamoDB 做高写事件流和时序,Redis 做缓存和实时计数器,Elasticsearch 做全文搜索。每个 NoSQL 存储都是为解决一个特定瓶颈而加入的,而非取代关系核心。

性能与扩展模式

NoSQL 里的横向分片

大多数 NoSQL 数据库自动或半自动处理分片。理解机制在出问题时很重要。Cassandra 里,一致性哈希把分区键分散到环上——但选得差的分区键造成热分区,一个节点吸收不成比例的流量份额。基本规则:分区键应有高基数和均匀分布的写负载。在 Cassandra 用低基数键(如"国家代码")作分区键最多造几百个分区,峰值时让大多数节点闲着。

DynamoDB 里,热分区表现为特定 key 上的 "ProvisionedThroughputExceededException" 错误。缓解是分区键分片:给 key 追加随机后缀(1–N)、跨带后缀的变体分散写,并跨所有变体 scatter-gather 读。难看但对病态负载(爆款帖子上的全局点赞计数器)有效。

复制与冲突解决

多副本存储面对写冲突问题:对同一 key 在不同副本上的两个写在它们同步前发生。解决策略,从最简单到最正确:

总结

NoSQL 不是关系数据库的替代——它是一套专门工具。从 SQL 开始;它把大多数用例处理得很好。在你有特定瓶颈时才动用 NoSQL:缓存速度查找用键值,灵活层级数据用文档,高写时序用列族,深度关系遍历用图。围绕你的访问模式建模数据,理解你运行在哪个一致性级别,并刻意决定在哪接受 BASE 而非 ACID。最好的工程师在同一系统里两者都用。

🎯 面试速答

解释 CAP 定理及"P 是强制的"是什么意思。任何分布式系统里网络分区都会发生——所以你不能牺牲 P。真正的取舍是 CP(分区期间返回错误或阻塞以保持一致,如 HBase)vs AP(返回可能陈旧的数据以保持可用,如 Cassandra)。PACELC 扩展它:即便没有分区,也有日常更重要的持续延迟/一致性取舍。
什么时候选列族而非文档存储?当你写吞吐极高(百万/秒)、读模式前期已知、且你能负担围绕查询设计表时。Cassandra 的 LSM-tree 写路径让写无论数据量多大都 O(1)——文档存储在高量时付随机写代价。
为什么不能一切都用 NoSQL?NoSQL 用多表 ACID 事务和灵活临时查询换规模和 schema 灵活性。若你的领域有复杂关系且需要 join 或跨实体事务,SQL 几乎总是更简单、更安全、运维更便宜。过早加 NoSQL 增加运维复杂度而无瓶颈可解。
什么是最终一致性、何时危险?最终一致意味着副本会随时间收敛,但读在过渡期可能返回陈旧数据。它对库存计数(两个线程都读到 1 并写 0,导致负库存)、金融余额,以及任何陈旧读导致现实世界动作的场景危险。对这些用强一致(Cassandra 的 QUORUM、DynamoDB 的强一致读)——以更高延迟为代价。
DynamoDB 的单表设计模式是什么?把所有实体类型存在一张表,用复合排序键(如 ORDER#2024-01PROFILE)共置相关记录,这样多个实体类型能在单次查询里取出而无 join。它需要前期访问模式分析,但为所有建模过的模式提供 O(1) 读。

← 上一篇
消息队列