本页定位 · NoSQL & Search

关系型数据库(见 tech-database)不是万能的:海量文档、超高并发缓存、时序指标、关系网络、全文检索,各有更适合的存储。NoSQL 不是"反 SQL",而是"按数据形状选存储"。本页覆盖文档库(MongoDB)、列族(Cassandra/HBase)、缓存与协调(Redis/etcd)、图库(Neo4j)、以及全文检索王者 Elasticsearch——补上 tech-database 没讲的半壁江山。我会把每个知识点拆成"是什么 → 为什么 → 怎么用 → 踩什么坑"。基线:MongoDB 7、Cassandra 4、Redis 7、Neo4j 5、Elasticsearch 8。

1

为什么是 NoSQL:CAP 与 AP/CP 取舍

CAP · AP / CP · 选型逻辑

NoSQL 的兴起不是因为 SQL"不好",而是因为某些数据形状用关系模型代价太高。先搞懂它背后的一致性哲学,才知道什么时候该用、什么时候别用。

NoSQL 到底"N"什么

NoSQL 最早指 "Not only SQL"(不仅是 SQL),强调不局限于关系模型。它涵盖文档、列族、KV、图、时序等多种数据模型。核心动机:① 海量数据要水平扩展(加机器而不是换大机器);② 结构多变、不想被固定 schema 绑住;③ 超高并发读写(缓存/计数);④ 特定查询形态(全文、图遍历)。它不是要取代关系库,而是补关系库不擅长的那一摊。

论别被名字误导

很多 NoSQL 现在也支持类 SQL 查询(如 MongoDB 的 aggregation、Cassandra 的 CQL、ES 的 SQL 接口)。"NoSQL" 指的是数据模型与扩展哲学,不是"不能写 SQL"。选型看的是模型与一致性,不是名字。

CAP 定理:三者最多取其二

分布式系统里:C 一致性(所有节点同一时刻看到相同数据)、A 可用性(每个请求都有响应,不保证最新)、P 分区容忍(网络断了系统还能跑)。现实网络一定会分区(P 不可免),所以真实取舍是 CP(保一致、分区时宁可拒绝,如 ZooKeeper/etcd/HBase)还是 AP(保可用、接受短暂不一致,如 Cassandra/DynamoDB)。

类型含义典型适合
CP保一致,分区时部分不可用ZooKeeper / etcd / HBase协调、元数据、账务
AP保可用,接受最终一致Cassandra / DynamoDB / Redis海量写入、会话、 feed
CA(理论)单点无分区才成立单机 MySQL非分布式场景
# 分区发生时 CAP 的取舍(示意) if network_partition: if cp_mode: reject_writes() # 保一致,牺牲部分可用 else: serve_stale() # 保可用,接受短暂旧值

BASE:AP 系统的哲学

AP 系统常用 BASE 概括:Basically Available(基本可用)、Soft state(软状态,允许中间不一致)、Eventually consistent(最终一致)。它和关系库的 ACID 是两种世界观:ACID 要"现在就绝对对",BASE 要"最终会对,中间可能旧"。多数互联网场景其实能接受 BASE。

# ACID vs BASE 一句话对照 ACID: 转账必须立刻双方都正确(强一致) BASE: 点赞数先 +1 返回,副本几秒后同步(最终一致) # 最终一致读:写主、读副本,容忍短暂旧值 def incr_likes(pid): mysql.incr(pid); mq.publish("like.changed", pid) # 读时可能短暂旧值,几秒追平——点赞数容忍

论什么时候必须 CP

钱、锁、元数据这类"错一点就出大事"的数据,必须 CP:分布式锁用 etcd/ZK(基于租约)、库存扣减要强一致、服务注册中心要一致。反过来,浏览量、推荐、草稿这类"旧一点无妨"的数据,AP 更划算。按"出错代价"定一致性目标,别一刀切。

一致性强弱谱:不止 CP/AP 两档

现实比 CP/AP 更细:线性一致(最强,如 etcd)、因果一致、会话一致(同一用户看到自己刚写的)、最终一致(最弱)。选多强的"一致性",直接决定性能与复杂度。多数业务用"会话一致 + 最终一致"就够,不必追线性一致。

选型的第一性原理

论先问数据形状,再问数据库

别从"哪个数据库火"出发。先列:数据结构固定吗?查询是点查还是扫描还是图遍历?要强一致吗?读多写多?答案出来,候选库自然缩小。NoSQL 的价值正是"为特定形状而生",用错形状比用错品牌更致命。

一个速查矩阵

数据形状首选一致性倾向
结构化、强一致、事务MySQL / PostgreSQLCP
半结构化文档 / 快速迭代 schemaMongoDBCP(单分片)/ 可调
海量宽表写入Cassandra / HBaseAP / CP
极热 KV / 缓存 / 协调Redis / etcdAP / CP
关系网络Neo4j—
全文检索 / 聚合ElasticsearchAP(近实时)

何时不该用 NoSQL:三个反模式

NoSQL 不是银弹。三种滥用:① 用 Cassandra 跑报表(ad-hoc 查询弱);② 用 MongoDB 硬 join(没有外键,反复 $lookup);③ 用 Redis 存必须一致的状态(会丢)。判断标准就一句:你的查询形态是不是这种存储的强项?不是就回头用关系库。

反模式正确选择
用列族库跑多维报表OLAP / ES
文档库反复 join关系库
Redis 存强一致状态etcd / 关系库
"新技术焦虑"驱动的选型

见过太多团队因为"同行都在用 X"就上 X,结果 80% 的数据关系库就能搞定,反而多了同步与运维负担。选型由数据形状驱动,不由流行度驱动。能一个关系库解决的,别上五个。

2

文档数据库:MongoDB 与灵活 schema

Document Model · 聚合管道 · 索引 · 事务 · 分片

文档库以 JSON 风格文档为单位存储,同集合里的文档结构可以不同——适合需求快速变化、嵌套结构多的场景(商品、内容、日志)。这一章讲清模型、聚合、索引、事务与分片。

文档模型:把"对象"原样存

关系库要把对象拆成多张表、用外键 join;文档库一个对象就是一个文档,嵌套、数组直接存,读一次就拿到,不用 join。这对"商品有多种属性、用户有可变 profile"这类场景极友好。

// MongoDB 文档示例 { "_id": "u1001", "name": "MJ", "tags": ["vip", "dev"], // 数组直接存 "profile": { "age": 30, "city": "SZ" }, // 嵌套对象 "orders": [ {"id":1,"amt":99} ] // 一对多也能嵌 }
灵活 ≠ 不设计

schema 自由容易变成"一团乱麻"。仍要规划好查询模式并建立索引;否则"灵活"会变成"慢查询温床"。MongoDB 也支持事务(多文档),但别拿它当 PostgreSQL 用——需要复杂 join / 强跨文档事务时,关系库更合适。

嵌套 vs 引用:建模的两难

文档库有两种关联方式:嵌套(embed)把子对象塞进父文档,读快、但数组会膨胀;引用(reference)存别的文档 id,像外键,灵活但要二次查询。经验:"一对一/一对少且常一起读"就嵌;"一对多且会无限增长/独立访问"就引用。

// 嵌:用户地址(少且常一起读) { "_id":"u1", "addr":{"city":"SZ","zip":"518000"} } // 引用:用户的订单(多且独立) { "_id":"o1", "uid":"u1", "amt":99 } // 查用户订单用 uid 索引
场景选理由
用户地址 / profile嵌套少、常一起读
订单列表引用多、独立增长
评论(无限)引用避免超宽文档
商品规格嵌套结构固定、同读

聚合管道:文档库的"SQL"

MongoDB 用聚合管道(aggregation pipeline)做复杂查询:一串 stage($match 过滤、$group 分组、$sort、$project 投影、$lookup 关联)像流水线一样串起来。比写命令式代码清晰,也能下推到索引。

// 按城市统计各用户订单总额,取前 5 db.orders.aggregate([ { "$match": { "status": "paid" } }, { "$group": { "_id": "$city", "total": { "$sum": "$amt" } } }, { "$sort": { "total": -1 } }, { "$limit": 5 } ])

论$lookup 不是免费 join

$lookup 能跨集合关联,但它不是关系库的 join——没有外键约束、无索引优化时是大开销,且跨分片更慢。如果只是常用的一对一关联,优先"嵌套"而非 $lookup。把文档库当关系库用,等于放弃它的优势。

// $lookup 跨集合关联(谨慎用) db.orders.aggregate([ { "$match": { "uid":"u1" } }, { "$lookup": { "from":"users", "localField":"uid", "foreignField":"_id", "as":"user" } } ]) // 频繁用就考虑嵌套,或冗余用户名进订单,避免每次 join

索引:查得快的关键

和关系库一样,没索引的查询是集合全扫描。MongoDB 支持单字段、复合、唯一、文本、地理空间索引。复合索引要遵循ESR 规则:等值(Equality)字段在前、排序(Sort)其次、范围(Range)最后,顺序错了索引用不上。

// 复合索引:city 等值、amt 范围 db.orders.createIndex({ "city": 1, "amt": -1 }) // 查 city=SZ 且 amt>100,能命中 db.orders.find({ "city":"SZ", "amt":{"$gt":100} }) // 用 explain 看是否走索引 db.orders.find(...).explain("executionStats")
索引不是越多越好

每个索引拖慢写入、占存储。文档库默认给 _id 建唯一索引,你自己加的索引要"为高频查询而建"。用 explain 验证是否真命中,别凭感觉建一堆用不上的索引。

事务:多文档也能 ACID(有限度)

MongoDB 4.0+ 支持多文档事务,4.2+ 支持分片事务。语法类似关系库:session.startTransaction() ... commitTransaction()。但它性能与范围有代价,且事务默认有 60s 限制、持有锁影响并发。能用一个文档嵌套搞定的,别上事务。

with client.start_session() as s: s.start_transaction() db.accounts.update_one({"_id":"A"}, {"$inc":{"bal":-10}}, session=s) db.accounts.update_one({"_id":"B"}, {"$inc":{"bal":10}}, session=s) s.commit_transaction()
# 事务要加重试与超时,避免永久卡死 for attempt in range(3): try: with client.start_session() as s: s.start_transaction(); ...; s.commit_transaction() break except OperationFailed: sleep(2 ** attempt) # 指数退避

分片:文档库也要水平扩展

单 mongod 容量/吞吐到顶时,分片集群(sharded cluster)把数据按 shard key 分布到多个 shard。选 shard key 的原则和关系库分片键一样:高基数、分布均匀、贴合查询。坏 key(如自增 id)会造成"热点块"。

// 建分片集合(示意) sh.shardCollection("shop.orders", { "user_id": "hashed" }) // hashed 让 user_id 均匀分布,避免范围分片热点

Change Stream:把变更流出去做同步

MongoDB 的 Change Stream 类似 CDC:监听集合的插入/更新/删除,实时推给消费者。它是"文档库 → 其它存储(ES/缓存/数据仓库)"同步的官方通道,比自己轮询高效得多,也是多语言持久化里文档库的"出口"。

# 监听 orders 集合的变更(Python 驱动) with db.orders.watch() as stream: for change in stream: if change["operationType"] == "insert": es.index("orders", change["fullDocument"]) # 同步到 ES

论Change Stream vs 应用双写

应用双写在业务代码里同时写多库,容易因异常漏写导致不一致;Change Stream 由数据库保证"写了就流出",业务无感知、更稳。多存储同步优先 CDC / Change Stream,双写只作简单场景兜底。

3

列族数据库:Cassandra 与 HBase

Wide Column · LSM-Tree · 写入路径

当写入量是"每秒百万条"、且要跨机房多副本,行式关系库就吃力了。列族(宽表)数据库用 LSM-Tree 把随机写变成顺序写,专为海量写入与横向扩展而生。本章对比 Cassandra(AP)与 HBase(CP)。

宽表模型:行键 + 列族 + 动态列

列族库的"表"由行键(row key)索引,每行里有列族,列族下是动态、稀疏的列。同一行的列可以完全不同——非常适合"同一用户的行为日志,列随时间无限增加"。查询基本只能按 row key(或 row key 范围)取,不支持任意列上的高效过滤。

# Cassandra 建表(CQL 很像 SQL,但模型是宽表) CREATE TABLE user_events ( user_id text, ts timeuuid, event text, PRIMARY KEY (user_id, ts) # 分区键 + 聚类键 ) WITH CLUSTERING ORDER BY (ts DESC);

论为什么"按查询建模"

关系库先建模再写查询;列族库反过来——先想清楚查询,再设计表。因为过滤只能高效发生在主键/分区键上,所以常为一个查询建一张表("查询驱动建模")。这让写入极其快,但 ad-hoc 查询弱。

LSM-Tree:写入为什么这么快

关系库 B+树每次写可能要随机改树节点(慢)。LSM-Tree 把写先进内存 MemTable,满了刷成不可变的有序文件(SSTable)顺序落盘(快),后台再compaction合并去重。读时可能要查内存 + 多个 SSTable,再用布隆过滤器跳过无关文件。结果:写极快、读稍慢(可被缓存补偿)。

阶段动作特点
写进 MemTable(内存)顺序、极快
刷盘MemTable → SSTable顺序 IO
读内存 + 多层 SSTable + 布隆可能多查,靠缓存
合并Compaction去重、回收空间
# LSM 的读放大 vs 写放大权衡 # 写多读少:Leveled 写放大高,但读快(层少) # 写少读多:SizeTiered 读放大高,但写快 # 选型本质是拿写/读放大换你最在意的那个

写入路径:一条写请求的一生

# Cassandra 写入路径(示意) write → CommitLog(磁盘WAL) → MemTable(内存) → MemTable 满 → flush 成 SSTable(磁盘) → 后台 Compaction 合并 SSTable # 先写 CommitLog 保证宕机不丢,再写内存,所以写入既快又安全
Compaction 风暴

写入量大时 Compaction 会和正常读写抢 IO/CPU,造成"读延迟突刺"。调参(compaction 策略、并发度)和监控 SSTable 数量是运维重点。新手常忽略这点,上线后半夜被延迟告警叫醒。

# 常用 compaction 策略(Cassandra) # SizeTiered:写多场景默认,合并相似大小 SSTable # Leveled:读多/更新多,减少读放大(写放大略增) ALTER TABLE user_events WITH compaction = { 'class': 'LeveledCompactionStrategy' }; # 调并发度避免 IO 风暴:compaction_throughput_mb_per_sec

Cassandra(AP)vs HBase(CP)

两者都用宽表 + LSM,但定位不同:Cassandra 多主、无中心、AP 倾向、易跨机房伸展,适合"写多、容忍最终一致"(物联网、feed);HBase 依赖 HDFS + ZooKeeper、CP 倾向、强一致行级读写,适合"和 Hadoop 生态一体的海量存储"(日志归档、画像)。

维度CassandraHBase
架构无中心多主Master/RegionServer + ZK
一致性AP(可调)CP(行级强一致)
部署独立、轻依赖 HDFS,重
适合高写、跨机房海量存储 + 分析

适用场景与不适用场景

论什么时候上列族库

上:写入量极大(时序、日志、事件流)、需要线性扩展、查询模式固定(按 row key)。别上:需要复杂 join、ad-hoc 报表、事务跨行——这些它天生弱,硬上就是和自己过不去。那种需求留给关系库/OLAP。

# 虚拟节点(vnode):每物理节点负责多段 token 区间 # num_tokens=256:扩缩容时数据更均匀地在节点间流动 # 避免"某节点恰好负责热点大段"的不均 ALTER KEYSPACE shop WITH replication = { 'class':'NetworkTopologyStrategy', 'dc1':3 };

数据建模反例

分区键选错 = 热点 + 无限宽行

Cassandra 的分区键决定数据落在哪个节点。若用"事件类型"这种低基数列做分区键,所有同类事件挤一个分区→热点节点;若 row key 设计成单一大账号,行会无限宽→超宽行读爆。务必用高基数、均匀分布的分区键,并给宽行加时间边界。

读写一致性级别:ONE / QUORUM / ALL

Cassandra 允许按请求调一致性级别,在性能与一致强度间自由取舍。ONE 只写/读一个副本,最快但可能读到旧值;QUORUM 过半数(读 + 写都 QUORUM 保证线性一致);ALL 全副本,最强但最慢、任一节点挂就不可用。多数场景用 QUORUM。

# 写/读都用 QUORUM:保证"写进的,读得到" INSERT INTO user_events (...) VALUES (...) USING CONSISTENCY QUORUM; SELECT * FROM user_events WHERE user_id='u1' USING CONSISTENCY QUORUM; # 可容忍旧值的热点读用 ONE 提吞吐

论读写一致性的"组合拳"

要保证强一致,规则是 写 QUORUM + 读 QUORUM(或写 ALL + 读 ONE)。因为 Quorum 读写必然在至少一个副本上相交,读到的一定含最新写。只写 ONE 读 ONE 最快但可能脏读——按数据重要性分级设置级别,而非全局一刀切。

4

KV 存储:Redis 与 etcd

In-Memory KV · 分布式协调

KV 是最简单的模型——一个 key 对应一个 value。但"简单"让它极快、极通用。本章分两半:Redis(内存 KV,扛热点与数据结构)与 etcd(一致 KV,做分布式协调)。

Redis:内存 KV 的多种面孔

Redis 表面是 KV,实际 value 可以是字符串、哈希、列表、集合、ZSet、Stream——前面数据库进阶页讲过它的缓存/锁/集群。这里强调它的另一面:数据结构服务器。用对结构,很多"业务难题"变一行命令。

# ZSet 做排行榜(自动按分数排序) ZADD leaderboard 100 "userA" ZADD leaderboard 200 "userB" ZREVRANGE leaderboard 0 9 WITHSCORES # Top 10 # Stream 做可靠消息流(消费组) XADD orders * "data" "{...}" XGROUP CREATE orders cg $

Redis 的应用形态清单

用法结构场景
缓存String/Hash商品详情、配置
分布式锁String (SET NX)防并发重复
排行榜ZSet积分、热度
限流ZSet/计数器滑动窗口
消息流Stream事件溯源
UV 估算HyperLogLog去重计数

几个容易被低估的 Redis 结构,用对能省大把空间:HyperLogLog 用 ~12KB 估算上亿 UV(误差约 0.8%,但不存具体谁);Bitmap 每位代表一天签到,统计极省。

# HyperLogLog 估算 UV(不存明细,极省内存) PFADD uv:2026-09-20 user1 user2 user3 PFCOUNT uv:2026-09-20 # 去重计数 # Bitmap 做签到(1 位 / 天 / 用户) SETBIT sign:user:1001 20 1 # 第 20 天签到 BITCOUNT sign:user:1001 # 当月签到天数
大 key 与热 key

大 key(如一个存了百万元素的 Hash)删除/序列化会阻塞单线程,拖垮整个实例;热 key(某个 key 被海量请求打)会让单节点成为瓶颈。解法:大 key 拆分、热 key 本地缓存 + 多副本。Redis 单线程,一个慢命令全实例都卡,务必警惕。

# 滑动窗口限流(ZSet:score=时间戳,统计窗口内请求数) def is_allowed(key, limit=100, window=60): now = time.time() pipe.zremrangebyscore(key, 0, now-window) # 清过期 cnt = pipe.zcard(key) if cnt < limit: pipe.zadd(key, {now: now}); return True return False

etcd:一致 KV 与分布式协调

etcd 是强一致、高可用的 KV 存储(基于 Raft),它慢但绝对可靠。Kubernetes 的所有元数据就存在 etcd 里。它不用来扛业务流量,而是用来存配置、服务发现、分布式锁、选主这类"少写、必须一致"的数据。

# etcd 读写与租约(锁/心跳) etcdctl put /config/timeout "30s" etcdctl get /config/timeout # 租约:key 绑定 lease,进程挂了 lease 过期 key 自动删(天然分布式锁) etcdctl lease grant 30 # 拿到 leaseID etcdctl put /lock/job1 "me" --lease="leaseID"

论Redis 锁 vs etcd 锁

Redis 锁基于 SET NX,快但主从切换可能丢锁(前面讲过)。etcd 锁基于 Raft 租约,强一致、不会因切换丢,且自动过期(lease)防死锁。结论:高频、可最终一致用 Redis;元数据、选主、必须一致用 etcd/ZK。别用错场景。

# etcd 分布式锁(租约 + 抢占,自带自动释放) lease = etcdctl lease grant 30 etcdctl put /lock/job "worker-7" --lease=$lease # 抢到锁的 worker 持有 lease;保活需定期 refresh # 进程挂 → lease 30s 后过期 → 锁自动释放,不会死锁

Raft:etcd 一致性的根基

etcd 用 Raft 协议保证多副本一致:一个 Leader 接收写、复制到多数派(quorum)才算成功;Leader 挂了自动选新 Leader。写要过半数节点,所以节点数通常奇数(3/5)。这让它慢于 AP 系统,但保证"你读到的不会是过时的少数派"。

# 部署 3 节点的 etcd 集群(奇数,允许 1 台挂) etcd --name n1 --initial-cluster n1=...:2380,n2=...:2380,n3=...:2380 \ --initial-cluster-token tkn --initial-cluster-state new

Watch 与配置热更新

etcd 的 Watch 机制让客户端订阅 key 变化,配置一改,所有监听方秒级收到——这是"配置中心 / 服务发现"的核心能力。比"轮询数据库"优雅且实时。

# 监听某前缀的变化(配置热更新) etcdctl watch /config/ # 此 key 树任何变化都会推送 # 应用侧:收到 event → 重新加载配置,无需重启

选型小结:内存 KV 与协调 KV 别混

别拿 Redis 当元数据存储

Redis AOF 默认每秒刷盘、主从异步,极端情况会丢或脏读。把"服务注册表/锁/配置"这类必须一致的数据放 Redis,集群抖动时会出现"两个 Leader"或"配置读到旧值"的诡异事故。协调类数据交给 etcd/ZK,这是它们存在的意义。

Redis Cluster:槽位分片与扩缩容

Redis Cluster 把数据分成 16384 个哈希槽,每个节点负责一部分槽。key 通过 CRC16(key) % 16384 落在某槽、进而某节点。扩缩容就是把槽迁移到新节点,迁移期间旧节点对新 key 返回 MOVED 重定向,业务无感知。

# 把槽 0-1000 从节点 A 迁移到新节点 B redis-cli --cluster reshard <A> \ --cluster-from <A-id> --cluster-to <B-id> \ --cluster-slots 1001 --cluster-yes # 客户端遇到 MOVED 会自动重定向到正确节点
Cluster 下多 key 操作的坑

Cluster 不支持跨槽的事务 / mget 多 key,除非这些 key 用 {hashtag} 强制落到同一槽(如 {user1}:name 和 {user1}:age)。忘了 hashtag 会导致 "CROSSSLOT 错误"。需要原子多 key 操作时,把相关 key 用同一 hash tag 包起来。

5

图数据库:Neo4j 与关系查询

Property Graph · Cypher · 遍历

"某人好友的好友里谁和我同公司"——这种多跳关系查询,用 SQL 要自连接 N 次,越跳越慢。图数据库把"关系"当一等公民,遍历边天然高效。社交、反欺诈、推荐、知识图谱都靠它。

属性图模型:点、边、属性

图由节点(Node)和关系(Relationship/边)组成,两者都能带属性。节点可有标签(如 Person、Company),关系有类型和方向(如 FOLLOWS、WORKS_AT)。"关系"不再是外键,而是数据库里真实存储、带索引的边——所以"顺着边走"极快。

// Neo4j 建模:人-工作于-公司,人-关注-人 (:Person {name:"MJ", age:30})-[:WORKS_AT]->(:Company {name:"MJ Inc"}) (:Person {name:"MJ"})-[:FOLLOWS]->(:Person {name:"Bob"})

论为什么图库快

关系库没有"边"这种结构,多跳要靠 join 拼表,join 成本随跳数指数涨。图库把边存成"指针",从节点 A 直接顺着指针走到邻居 B、C——遍历成本只和"实际关联数"有关,和总数据量无关。数据越大、跳越多,优势越夸张。

Cypher:声明式图查询语言

Neo4j 用 Cypher,用 () 表节点、--> 表有向边、[] 表关系类型,像画 ascii 图一样写查询。比 SQL 的自连接直观太多。

// 查 MJ 关注的、且在同一公司的人(两跳) MATCH (me:Person {name:"MJ"})-[:FOLLOWS]->(f:Person), (me)-[:WORKS_AT]->(c:Company)<-[:WORKS_AT]-(f) RETURN f.name // SQL 里这要 Person 自连接 + 两表 join,图库一行遍历搞定
// 创建带权关系,便于最短路径 / 资金链路分析 MATCH (a:Account {id:"acc1"}), (b:Account {id:"acc2"}) CREATE (a)-[:TRANSFER {amount:500, ts:timestamp()}]->(b) // 带方向与属性的关系,让反欺诈可顺藤摸瓜向上遍历

遍历与最短路径

图库天生擅长路径类查询:最短路径(Dijkstra/BFS)、社区发现、环路检测。反欺诈里"这笔交易的上游资金链"就是典型:顺着转账边向上遍历,几跳内找到可疑源头。

// 两人之间的最短关系路径 MATCH p = shortestPath( (a:Person {name:"MJ"})-[*]-(b:Person {name:"Bob"})) RETURN p // 反欺诈:沿转账边向上遍历 N 跳 MATCH (x:Account {id:"acc1"})-[t:TRANSFER*1..5]->(src) RETURN src, reduce(s=0, n IN t | s + n.amount)

索引与约束

图库不是"免索引"。起点查询("找 name=MJ 的人")仍要靠节点索引/约束,否则全图扫描。常用 CREATE INDEX 和 CREATE CONSTRAINT(如唯一约束防重复节点)。

// 给 Person.name 建索引,起点查询才快 CREATE INDEX person_name FOR (p:Person) ON (p.name) // 唯一约束:防止重复创建同一用户节点 CREATE CONSTRAINT person_id UNIQUE FOR (p:Person) ON (p.id)
// 带权最短路径(按转账金额加权,找资金最短链路) MATCH p = shortestPath( (a:Account {id:"acc1"})-[r:TRANSFER*]-(b:Account)) RETURN p, reduce(s=0, e IN r | s + e.amount) AS total // 默认按跳数;用权重需 GDS 的 weighted shortestPath 变体
无起点的"全图遍历"是性能炸弹

如果查询没有可以利用索引的起点(如"找出所有互相认识的人"),Neo4j 要扫全图,数据一大就爆。务必让高频查询带有索引的锚点(如先定位某个用户/账户,再向外遍历)。别写"无边界 * 多跳"的野查询。

什么时候用图库、什么时候别

场景该用图库吗理由
社交好友/关注✅多跳关系
反欺诈资金链✅路径遍历
知识图谱 / RAG✅实体关系
订单 CRUD❌关系库更合适
海量宽表写入❌列族库更合适

落地形态:图库常作"关系层"

生产里图库通常不是"主库",而是关系层:主数据在关系/文档库,把"谁和谁有关"同步进图库做关系查询。比如推荐系统:用户行为进图库,实时算"相似用户",再把结果回写业务库。它和 ES 一样是"专才",不是"通才"。

图算法:不止查询,还能挖掘结构

图库除了遍历查询,还内置图算法:PageRank 算节点影响力(识别关键账户/KOL)、社区发现(Louvain)找紧密子群(社群划分)、最短路径算距离。这些在反欺诈(资金网络核心节点)、推荐(相似社群)、知识图谱里极有用。

# Neo4j GDS:跑 PageRank 找影响力节点 CALL gds.pageRank.stream('accountGraph') YIELD nodeId, score RETURN gds.util.asNode(nodeId).name AS name, score ORDER BY score DESC LIMIT 10; # 社区发现:把资金网络划分成团伙 CALL gds.louvain.stream('accountGraph') YIELD nodeId, communityId

论图算法 vs 关系库自连接

关系库算"影响力/社群"要写极复杂的递归 CTE 或应用层迭代,数据一大就爆;图库把图结构常驻内存,算法是原生算子,几千万边也能秒级出结果。这属于"用对模型省一年开发"的典型。

6

选型矩阵与混合持久化落地

Polyglot in Practice

前面五种模型都讲了。这一章把它们收口成一张可执行的选型矩阵,并讲清"多语言持久化"在真实系统里怎么拼、怎么同步、怎么不把自己搞死。

一张能直接用的选型矩阵

把全页内容压成一张表,下次选型照着对:

数据形状首选一致性别用它做
结构化、事务MySQL/PostgreSQLCP全文检索
半结构化文档MongoDB可调复杂 join
海量宽表写入Cassandra/HBaseAP/CPad-hoc 查询
极热 KV/缓存RedisAP元数据存储
协调/配置/锁etcd/ZKCP业务流量
关系网络Neo4j—通用 CRUD
全文/聚合ElasticsearchAP主数据库
时序指标InfluxDBAP事务

混合持久化的真实拼盘

一个电商/内容平台的典型组合:主数据(用户/订单)→ PostgreSQL;商品详情/草稿 → MongoDB;热点缓存/会话/锁 → Redis;搜索 → Elasticsearch;监控指标 → 时序库;推荐关系 → Neo4j;配置/选主 → etcd。每种只干最擅长的事。

论它们怎么保持一致

核心心法:源唯一。关系/文档库是强一致源,其余都是副本。变更发生后通过 CDC(Canal/Debezium)+ MQ 异步同步到 ES/图库/缓存;每条消费链路都要幂等,再用对账兜底。没有"实时强一致同步所有库",而是"源唯一、副本异步追、最终一致"。

同步的三种姿势

方式做法优劣
应用双写业务里同时写多库简单但易不一致
CDC 同步监听 binlog 喂副本解耦、稳(推荐)
定时批同步离线任务搬运最弱,仅非实时
源(唯一真相)管道副本目标用途
MySQL binlogDebezium → Kafkaes_consumer更新检索
MySQL binlogDebezium → Kafkagraph_consumer更新关系网络
MySQL binlogCanal → MQcache_refresh刷新热点缓存

业务代码完全不感知这些副本——它只写源库,源库是唯一真相。副本延迟几秒对搜索、推荐、缓存通常可接受,靠 幂等消费 + 对账 兜底做到最终一致。这也是为什么"应用双写"最危险:业务里同时写多库,任何一次失败都会留下永久不一致,还不如让 CDC 统一搬运。

不要把架构搞成"全家桶灾难"

存储越多,债务越多

每加一种存储,就多一套部署、监控、备份、告警、人员学习成本。新手最容易"看啥潮用啥",结果 8 种数据库互相同步、没人敢动。原则:能用一个关系库解决的,别上五个;新存储必须有 owner、监控、降级方案,否则宁可不上。

一致性分级:别一刀切

同一系统里不同数据定不同一致性目标:余额/锁要 CP(etcd/关系事务);商品评分/PV 接受 AP(最终一致)。按"出错代价"分级,而不是给所有数据同目标——那既浪费又没必要。出错代价高(钱、锁)就 CP,出错代价低(计数、热度)就 AP,下面是常用的分级参考:

数据目标容忍
余额 / 锁CP几乎零
订单状态CP 源 + 近似读秒级
商品评分AP分钟级
PV / UVAP可估算

落地清单与下一步

路动手前先过这五关

① 数据形状定模型;② 一致性定 CP/AP;③ 查询形态定能否命中索引;④ 规模定要不要分片/集群;⑤ 运维定谁扛、挂了怎么降级。五关过了,选型自然稳。多语言持久化的精髓不是"用很多库",而是"每个库都用对地方"。

成本与团队:让架构可持续

多语言持久化的最大隐性成本是人。每多一种存储,团队就要有人懂它的运维、调优、故障处理。选型的终点是"团队扛得住":小团队优先少而精(一个关系库 + 一个缓存 + 一个检索),大团队才按瓶颈逐个引入专才存储。

团队规模推荐存储组合
小(<10 人)PostgreSQL + Redis + ES
中+ MongoDB(文档场景)
大+ Cassandra / Neo4j / etcd 按瓶颈加
没有 owner 的存储 = 定时炸弹

每种存储上线前必须明确 owner(谁负责容量、告警、升级、故障演练)。"大家都能看一眼"等于"没人负责"。没 owner 的数据库,出问题时没人敢动、恢复最慢。这是比技术选型更常被忽视的翻车点。

结
本页要点

NoSQL 的本质是"按数据形状选存储"(多语言持久化):CAP 决定 CP/AP 取舍,AP 系统用 BASE 接受最终一致。文档库 MongoDB 用嵌套/引用建模、聚合管道做查询、注意 ESR 索引与事务代价、shard key 选高基数;列族库 Cassandra/HBase 用宽表 + LSM-Tree 把随机写变顺序写、适合海量写入但查询必须按 row key,Cassandra 偏 AP、HBase 偏 CP;KV 里 Redis 是内存数据结构服务器(大 key/热 key 要防),etcd 是一致 KV 做协调/锁/配置(基于 Raft 租约,强于 Redis 锁);图库 Neo4j 把关系当一等公民,Cypher 多跳遍历天然快,但要有索引锚点避免全图扫描;Elasticsearch 以倒排索引称霸搜索与聚合。落地原则:源唯一、副本异步追、按出错代价分级一致性、每个存储有监控有降级。

自测 · 看你是否真懂

1.什么场景该选文档库而不是关系库?MongoDB 的嵌套和引用怎么选?

查看答案

数据结构多变、嵌套深、schema 迭代快(商品属性、草稿、日志)且不强依赖跨文档事务时选文档库。建模上:一对一/一对少且常一起读就"嵌套"(embed),一对多且会无限增长/独立访问就"引用"(reference)。需要强一致事务与复杂 join 仍应选关系库。

2.Cassandra 和 HBase 都是列族库,为什么一个 AP 一个 CP?分别适合什么?

查看答案

两者都用宽表 + LSM-Tree,但 Cassandra 无中心多主、AP 倾向、易跨机房,适合高写/可最终一致(物联网、feed);HBase 依赖 HDFS+ZK、CP 倾向、行级强一致,适合与 Hadoop 一体的海量存储与分析。选错一致性模型会出数据正确性或扩展性问题。

3.Redis 锁和 etcd 锁哪个更"稳"?为什么?

查看答案

etcd 锁更稳。Redis 锁基于 SET NX,主从异步复制时主挂锁可能丢;etcd 基于 Raft 租约,强一致且租约过期自动释放防死锁。所以:高频可最终一致用 Redis;元数据/选主/必须一致用 etcd/ZK。

4.为什么"好友的好友"这类多跳查询用图库比 SQL 快?

查看答案

关系库的"边"不存在,多跳要靠自连接拼表,成本随跳数指数涨;图库把关系存成带索引的指针,从节点顺着边走到邻居,遍历成本只和"实际关联数"有关、与总数据量无关。数据越大跳越多,优势越明显。但查询必须有索引锚点,否则全图扫描。

5.混合持久化里"源唯一、副本异步追"是什么意思?为什么不能直接多库强一致同步?

查看答案

让关系/文档库作为强一致"源",ES/图库/缓存都是它的异步副本,通过 CDC+MQ 同步,消费端幂等、对账兜底。因为跨库强一致同步代价极高(2PC 阻塞、单点),且多数副本查询容忍几秒延迟,最终一致 + 幂等 + 对账在实践中更稳更可扩展。

下一步往哪走

路学完 NoSQL 之后

① 回 tech-database:那里讲 MySQL/PG/Redis 基础与分布式事务、分库分表,本页是它的"另一半"(文档/列族/图/协调)。

② 串系统设计:缓存、分片、读写分离在本页是"单点技术",在 tech-systemdesign 是"架构拼图",配合看才成体系。

③ 动手:用 Docker 起 MongoDB + Redis + etcd + Neo4j + ES,写个小项目把五种存储各用一遍,体会"各司其职 + CDC 同步"。

④ 深入可观测:多存储架构的监控基线(命中率、写入延迟、副本延迟)是调优前提,别等线上炸了才看。