NoSQL数据库的兴起源于互联网规模应用对传统关系型数据库的挑战。当面对数十亿用户、PB级数据、毫秒级响应要求时,传统的ACID事务模型和严格的模式约束成为了扩展性的瓶颈。本章深入探讨各类NoSQL数据库的设计原理、实现技术和最佳实践,帮助您理解如何为不同的数据模型和访问模式选择合适的存储方案。
NoSQL的发展经历了三个关键阶段:
第一代(2000-2005):Memcached、Berkeley DB等简单键值存储,主要解决缓存问题。这一代系统功能单一,主要作为关系型数据库的补充。
第二代(2006-2010):Google BigTable、Amazon Dynamo论文引发革命,Cassandra、HBase、MongoDB等系统涌现。这一代开始探索不同的数据模型和一致性权衡。
第三代(2011-现在):NewSQL和多模型数据库兴起,如CockroachDB、FaunaDB。追求在保持可扩展性的同时提供更强的一致性保证和SQL支持。
NoSQL(Not Only SQL)并非完全抛弃SQL,而是针对特定场景优化的数据管理方案。其核心理念包括:
CAP理论由Eric Brewer提出,Seth Gilbert和Nancy Lynch给出了正式证明。理解CAP需要明确各项的准确含义:
一致性(Consistency):所有节点在同一时刻看到相同的数据。这里指的是线性一致性(Linearizability),是最强的一致性保证。
可用性(Availability):每个请求都能收到响应,不管成功或失败。注意这要求所有非故障节点都必须响应。
分区容错性(Partition Tolerance):系统在网络分区的情况下继续运行。在分布式系统中,网络分区是不可避免的,因此P实际上是必选项。
一致性(C)
/\
/ \
/ \
/ CA \ ← 单机系统
/ \
/ P \
/ \
/ CP AP \
/________________\
可用性(A) 分区容错(P)
实际选择:
CP系统:HBase, MongoDB (主节点模式)
AP系统:Cassandra, DynamoDB
CA系统:单机关系型数据库(不考虑网络分区)
实践中的CAP权衡比理论更复杂:
强 ← → 弱
线性一致性 > 顺序一致性 > 因果一致性 > 最终一致性
BASE(Basically Available, Soft state, Eventual consistency)是NoSQL系统的设计原则:
| 维度 | RDBMS | NoSQL |
|---|---|---|
| 数据模型 | 固定模式,规范化 | 灵活模式,反规范化 |
| 查询能力 | 复杂SQL,JOIN支持 | 简单查询,应用层JOIN |
| 事务支持 | ACID,强一致性 | BASE,最终一致性 |
| 扩展方式 | 垂直扩展为主 | 水平扩展为主 |
| 性能特点 | 复杂查询优化 | 简单操作高吞吐 |
| 运维成本 | 单机简单,集群复杂 | 分布式原生,自动化程度高 |
选择NoSQL数据库的决策框架:
文档数据库将数据组织为文档集合,每个文档是自包含的数据单元,通常以JSON、BSON或XML格式存储。MongoDB、CouchDB和Amazon DocumentDB是典型代表。文档模型特别适合处理层次化、半结构化的数据,避免了关系型数据库中复杂的JOIN操作。
文档数据库的设计哲学源于三个关键观察:
文档模型的核心优势是其嵌套结构能够自然地表达复杂对象:
{
"_id": "user_123",
"name": "张三",
"profile": {
"age": 28,
"email": "zhang@example.com",
"preferences": {
"language": "zh-CN",
"timezone": "Asia/Shanghai",
"notifications": {
"email": true,
"sms": false,
"push": true
}
},
"addresses": [
{
"type": "home",
"city": "北京",
"street": "中关村大街1号",
"primary": true,
"coordinates": {
"lat": 39.9042,
"lng": 116.4074
}
},
{
"type": "work",
"city": "上海",
"street": "南京路100号",
"primary": false
}
]
},
"orders": [
{
"order_id": "ord_456",
"date": "2024-01-15",
"status": "delivered",
"total": 299.99,
"items": [
{
"product_id": "prod_789",
"name": "无线耳机",
"price": 299.99,
"quantity": 1
}
]
}
],
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
有效的设计模式:
// 好的例子:用户和其设置
{
"user_id": "123",
"settings": {
"theme": "dark",
"language": "zh-CN"
}
}
适用场景:
// 用户文档
{
"user_id": "123",
"name": "张三",
"post_ids": ["post_1", "post_2", "post_3"]
}
// 独立的帖子文档
{
"post_id": "post_1",
"author_id": "123",
"content": "..."
}
适用场景:
// 存储最近N条,完整列表用引用
{
"user_id": "123",
"recent_orders": [
{"order_id": "o1", "date": "2024-01", "total": 100},
{"order_id": "o2", "date": "2024-01", "total": 200}
],
"all_order_ids": ["o1", "o2", "o3", "o4", ...]
}
// 时序数据分桶存储
{
"sensor_id": "sensor_001",
"start_time": "2024-01-15T00:00:00Z",
"end_time": "2024-01-15T01:00:00Z",
"measurements": [
{"timestamp": "00:00:00", "temp": 25.1},
{"timestamp": "00:00:10", "temp": 25.2},
// ... 最多360个测量值(1小时,10秒间隔)
],
"count": 360,
"sum_temp": 9036,
"min_temp": 24.8,
"max_temp": 25.5
}
设计原则:
数据关系分析
│
├─ 是否一起访问?
│ ├─ 总是 → 考虑嵌入
│ └─ 偶尔 → 考虑引用
│
├─ 更新频率?
│ ├─ 子文档独立更新 → 引用
│ └─ 一起更新 → 嵌入
│
├─ 数据大小?
│ ├─ 可能超16MB → 引用
│ └─ 远小于16MB → 嵌入
│
└─ 一对多基数?
├─ <100 → 嵌入
├─ 100-1000 → 混合
└─ >1000 → 引用
BSON(Binary JSON)是JSON的二进制编码,提供了额外的数据类型和高效的遍历:
BSON文档结构:
┌────────────┬────────────┬────────────┬─────┐
│ 总长度 │ 元素1 │ 元素2 │ ... │
│ (4字节) │ │ │ │
└────────────┴────────────┴────────────┴─────┘
元素结构:
┌──────┬───────────┬───────────┬───────┐
│ 类型 │ 字段名 │ 值 │ \x00 │
│(1字节)│ (字符串) │ (变长) │ │
└──────┴───────────┴───────────┴───────┘
BSON的优势:
文档数据库的索引比关系型数据库更加灵活:
db.users.createIndex({"name": 1})
db.users.createIndex({"profile.age": 1, "profile.city": 1})
db.users.createIndex({"profile.addresses.city": 1})
db.articles.createIndex({"content": "text"})
db.places.createIndex({"location": "2dsphere"})
文档数据库的查询处理需要处理嵌套结构和数组:
查询计划生成:
查询:db.users.find({"profile.age": {$gt: 25}, "orders.total": {$gt: 1000}})
执行计划:
┌─────────────────┐
│ Collection │
│ Scan │
└────────┬────────┘
│
┌────▼────┐
│ Filter │ profile.age > 25
└────┬────┘
│
┌────▼────┐
│ Filter │ orders.total > 1000
└────┬────┘
│
┌────▼────┐
│ Project │
└─────────┘
查询优化技术:
文档数据库通过分片实现水平扩展:
分片架构:
┌──────────────────────────────────────┐
│ 应用程序 │
└────────────┬─────────────────────────┘
│
┌────────▼────────┐
│ Mongos路由 │
└────┬───────┬────┘
│ │
┌────▼───┐ ┌▼─────┐
│ Shard1 │ │Shard2│
│ RS1 │ │ RS2 │
└────────┘ └──────┘
分片键选择原则:
复制集机制:
列族存储(Wide Column Store)如Cassandra、HBase和Amazon DynamoDB,采用了不同于行存储的数据组织方式,特别适合处理稀疏数据和高吞吐量写入。
列族存储的数据模型可以理解为一个多维映射:
(RowKey, ColumnFamily:Column, Timestamp) → Value
逻辑视图:
RowKey: user_123
┌─────────────────────────────────────────────────────┐
│ ColumnFamily: profile │
├──────────────┬──────────────┬──────────────────────┤
│ name: │ age: │ email: │
│ "张三" │ 28 │ "zhang@example.com" │
├──────────────┴──────────────┴──────────────────────┤
│ ColumnFamily: activity │
├──────────────┬──────────────┬──────────────────────┤
│ login:2024:01│ login:2024:02│ purchase:2024:01:15 │
│ 15 │ 8 │ "order_456" │
└──────────────┴──────────────┴──────────────────────┘
设计特点:
列族存储普遍采用LSM(Log-Structured Merge)树结构:
写入路径:
写入请求
│
▼
┌─────────┐
│ MemTable│ (内存,可变)
└────┬────┘
│ 达到阈值
▼
┌─────────┐
│SSTable 0│ (磁盘,不可变)
└────┬────┘
│ Compaction
▼
┌─────────────────────┐
│ SSTable Level 1 │
└──────────┬──────────┘
│
┌─────▼─────┐
│ Level 2 │
└───────────┘
SSTable(Sorted String Table)结构:
┌──────────────────────────────────────┐
│ 数据块 (Data Blocks) │
│ ┌────────┬────────┬────────┐ │
│ │ Block1 │ Block2 │ Block3 │ ... │
│ └────────┴────────┴────────┘ │
├──────────────────────────────────────┤
│ 索引块 (Index Block) │
│ 记录每个数据块的起始key │
├──────────────────────────────────────┤
│ 布隆过滤器 (Bloom Filter) │
│ 快速判断key是否可能存在 │
├──────────────────────────────────────┤
│ 元数据 (Metadata) │
│ 文件信息、统计数据等 │
└──────────────────────────────────────┘
Compaction是LSM树的核心操作,用于合并SSTable和清理过期数据:
布隆过滤器用于快速判断key是否可能存在于SSTable中:
布隆过滤器原理:
输入: key = "user_123"
↓
哈希函数
h1, h2, h3
↓
位数组: [0,1,0,1,1,0,1,0,1,0,1,1]
↑ ↑ ↑
h1 h2 h3
判断:所有位置都为1 → 可能存在
任一位置为0 → 肯定不存在
参数调优:
写入优化:
读取优化:
读取请求
↓
MemTable (最新数据)
↓ 未命中
Block Cache (热数据块)
↓ 未命中
SSTable (磁盘文件)
列族存储的列式组织天然适合压缩:
原始: user_001_profile, user_001_activity, user_002_profile
压缩: user_001_profile, [8]activity, user_002_profile
原始: Beijing, Shanghai, Beijing, Beijing, Shanghai
字典: {0: Beijing, 1: Shanghai}
编码: 0, 1, 0, 0, 1
原始: 0,0,0,0,1,1,1,2,2,2,2,2
编码: (0,4), (1,3), (2,5)
图数据库如Neo4j、Amazon Neptune和ArangoDB专门用于存储和查询高度互联的数据。社交网络、知识图谱、推荐系统等场景中,实体间的关系往往比实体本身更重要。
图数据库主要有两种存储模型:
1. 原生图存储(Native Graph Storage)
节点存储:
┌────────┬────────┬──────────┬────────────┐
│ NodeID │ Labels │Properties│ Relations │
├────────┼────────┼──────────┼────────────┤
│ 1 │ Person │ {name: │ →[2,3,4] │
│ │ │ "张三"} │ │
└────────┴────────┴──────────┴────────────┘
边存储:
┌────────┬────────┬────────┬──────────┬─────────┐
│ EdgeID │ Type │ Source │ Target │Properties│
├────────┼────────┼────────┼──────────┼─────────┤
│ 101 │ KNOWS │ 1 │ 2 │{since: │
│ │ │ │ │ 2020} │
└────────┴────────┴────────┴──────────┴─────────┘
2. 索引邻接列表(Index-Free Adjacency)
节点记录:
┌──────────────────────────────────┐
│ NodeID: 1 │
│ Properties: {name: "张三"} │
│ FirstOutEdge: →Edge_101 │
│ FirstInEdge: →Edge_205 │
└──────────────────────────────────┘
│
▼
边记录(双向链表):
┌──────────────────────────────────┐
│ EdgeID: 101 │
│ Type: KNOWS │
│ StartNode: →Node_1 │
│ EndNode: →Node_2 │
│ NextOutEdge: →Edge_102 │
│ NextInEdge: NULL │
│ Properties: {since: 2020} │
└──────────────────────────────────┘
深度优先搜索(DFS)
DFS(start_node, visited=∅):
visited.add(start_node)
for neighbor in start_node.neighbors:
if neighbor ∉ visited:
DFS(neighbor, visited)
栈实现:
┌─────┐
│ A │ ← 当前节点
├─────┤
│ B │
├─────┤
│ C │
└─────┘
广度优先搜索(BFS)
BFS(start_node):
queue = [start_node]
visited = {start_node}
while queue ≠ ∅:
node = queue.dequeue()
for neighbor in node.neighbors:
if neighbor ∉ visited:
visited.add(neighbor)
queue.enqueue(neighbor)
队列实现:
← [D, E, F] ← 出队方向
↑
入队方向
双向搜索优化 从起点和终点同时搜索,相遇即找到路径:
起点搜索空间 终点搜索空间
○─○─● ●─○─○
/│\ │ │ /│\
○─○─○ ● ● ○─○─○
↑ ↑
└───相遇点──┘
搜索空间:O(b^(d/2)) vs O(b^d)
Dijkstra算法 适用于非负权重图:
优先队列状态:
┌──────┬──────┬────────┐
│ Node │ Dist │ Parent │
├──────┼──────┼────────┤
│ A │ 0 │ NULL │
│ B │ 5 │ A │
│ C │ 8 │ B │
│ D │ ∞ │ NULL │
└──────┴──────┴────────┘
松弛操作:
if dist[u] + weight(u,v) < dist[v]:
dist[v] = dist[u] + weight(u,v)
parent[v] = u
A*算法 使用启发式函数加速搜索:
f(n) = g(n) + h(n)
- g(n): 起点到n的实际距离
- h(n): n到终点的估计距离(启发式)
常用启发式函数:
- 欧几里得距离: √((x₂-x₁)² + (y₂-y₁)²)
- 曼哈顿距离: |x₂-x₁| + |y₂-y₁|
Floyd-Warshall算法 计算所有点对最短路径:
for k in 1..n:
for i in 1..n:
for j in 1..n:
dist[i][j] = min(dist[i][j],
dist[i][k] + dist[k][j])
动态规划思想:考虑是否经过中间节点k
标签传播算法(Label Propagation)
初始化:每个节点唯一标签
迭代:
1. 随机排序节点
2. 每个节点采用邻居中最频繁的标签
3. 重复直到收敛
示例迭代:
迭代0: [1][2][3][4][5]
迭代1: [1][1][3][3][5]
迭代2: [1][1][3][3][3]
收敛: 社区1:{1,2}, 社区2:{3,4,5}
Louvain算法 基于模块度优化的社区检测:
模块度 Q = 1/2m ∑ᵢⱼ[Aᵢⱼ - kᵢkⱼ/2m]δ(cᵢ,cⱼ)
两阶段迭代:
1. 局部移动:节点移动到能最大化模块度的社区
2. 社区聚合:将社区压缩为超节点,构建新图
┌───────┐ ┌───────┐ ┌───┐
│细粒度 │ --> │中粒度 │ --> │粗粒度│
│ 社区 │ │ 社区 │ │社区│
└───────┘ └───────┘ └───┘
Pregel模型(Bulk Synchronous Parallel)
超步(Superstep)执行:
┌─────────────────────────────┐
│ Compute (本地计算) │
├─────────────────────────────┤
│ Send Messages (发送消息) │
├─────────────────────────────┤
│ Barrier (全局同步) │
└─────────────────────────────┘
↓ 下一超步
顶点状态机:
Active → Compute → Active/Inactive
↑ │
└──收到消息────┘
图分区策略
节点分配到不同分区,边可能跨分区
分区1: {A, B} 分区2: {C, D}
跨分区边: B→C
边分配到分区,高度节点可能复制
分区1: A→B, A→C
分区2: A→D, B→D
节点A在两个分区都有副本
模式匹配优化
查询模式:
MATCH (a:Person)-[:KNOWS]->(b:Person)-[:LIKES]->(c:Product)
WHERE a.age > 25
执行计划:
1. 索引扫描: Person.age > 25
2. 扩展KNOWS关系
3. 过滤Person标签
4. 扩展LIKES关系
5. 过滤Product标签
优化技术:
- 选择性估计:先处理选择性高的条件
- 关系方向:利用出入度信息
- 标签索引:快速定位特定类型节点
时序数据库(TSDB)如InfluxDB、TimescaleDB、Prometheus专门优化了时间序列数据的存储和查询。物联网、监控系统、金融市场等场景产生的数据具有时间戳、高写入量、查询模式可预测等特点。
时序数据具有独特的特征,需要专门的优化策略:
典型时序数据结构:
┌────────────┬────────────┬────────────┬────────┐
│ Timestamp │ Metric │ Tags │ Value │
├────────────┼────────────┼────────────┼────────┤
│2024-01-15 │cpu.usage │host=srv1 │ 78.5 │
│10:30:00 │ │region=east │ │
└────────────┴────────────┴────────────┴────────┘
分段存储(Segmented Storage)
时间分段:
├── 2024-01-15/
│ ├── 00-06/ (活跃段,可写)
│ ├── 06-12/ (只读段)
│ └── 12-18/ (只读段)
├── 2024-01-14/
│ └── ... (压缩存储)
└── 2024-01-13/
└── ... (降采样)
段内结构:
┌─────────────────────────────┐
│ 索引文件 (Index) │
│ metric → series_id映射 │
├─────────────────────────────┤
│ 数据文件 (Data) │
│ 时间戳数组 | 值数组 │
├─────────────────────────────┤
│ 元数据 (Metadata) │
│ 统计信息、压缩参数 │
└─────────────────────────────┘
列式存储优化
行式存储(传统):
┌──────┬──────┬──────┬──────┐
│ t1 │ m1 │ tag1 │ v1 │
├──────┼──────┼──────┼──────┤
│ t2 │ m1 │ tag1 │ v2 │
└──────┴──────┴──────┴──────┘
列式存储(TSDB):
时间戳列: [t1, t2, t3, ...]
值列: [v1, v2, v3, ...]
标签索引: tag1 → [series_1, series_2]
优势:
- 同类数据连续,压缩率高
- 批量聚合操作效率高
- 可以独立压缩每列
时序数据的规律性使其特别适合压缩:
1. Delta编码 存储相邻值的差值:
原始时间戳: 1705294200, 1705294260, 1705294320
Delta编码: 1705294200, 60, 60
进一步压缩: 1705294200, [60] × 2
2. Gorilla压缩(Facebook) 利用浮点数的位模式相似性:
XOR压缩:
值1: 12.5 = 0x41480000
值2: 12.6 = 0x41499999
XOR: 0x00019999 (大量前导零)
压缩格式:
┌───┬────────┬─────────┐
│控制│前导零数│有效位 │
│位 │(5 bits)│(variable)│
└───┴────────┴─────────┘
3. 字典压缩 对重复的标签值使用字典:
原始: host=server1, host=server1, host=server2
字典: {0: "server1", 1: "server2"}
编码: host=0, host=0, host=1
降采样策略
原始数据(1秒精度):
10:00:00 → 100
10:00:01 → 102
10:00:02 → 98
10:00:03 → 101
...
1分钟聚合:
10:00:00 → avg:100.25, min:98, max:102, count:60
1小时聚合:
10:00:00 → avg:99.8, min:85, max:115, p95:110
保留策略:
- 原始数据:保留7天
- 1分钟聚合:保留30天
- 1小时聚合:保留1年
连续聚合(Continuous Aggregation)
实时聚合管道:
原始数据流
│
┌───────┴───────┐
▼ ▼
实时表 聚合任务
(最近数据) │
│ ▼
│ 物化视图
│ (历史聚合)
└───────┬───────┘
▼
查询合并
倒排索引(Inverted Index)
Series索引:
metric_name + tags → series_id
倒排索引:
tag_key=tag_value → [series_id1, series_id2, ...]
示例:
region=east → [1, 3, 5, 7]
type=cpu → [1, 2, 3, 4]
查询 region=east AND type=cpu:
result = [1, 3, 5, 7] ∩ [1, 2, 3, 4] = [1, 3]
时间索引
分层时间索引:
年 → 月 → 日 → 小时 → 分片
稀疏索引(每N个点一个索引项):
┌──────────┬─────────┐
│Timestamp │ Offset │
├──────────┼─────────┤
│10:00:00 │ 0 │
│10:01:00 │ 1024 │
│10:02:00 │ 2048 │
└──────────┴─────────┘
向下采样查询(Downsampling Query)
查询:1年的CPU使用率趋势
优化路径:
if 时间范围 > 30天:
使用1小时聚合表
elif 时间范围 > 7天:
使用1分钟聚合表
else:
使用原始数据
自动选择合适粒度,平衡精度和性能
时间范围分区裁剪
查询:WHERE time >= '2024-01-10' AND time <= '2024-01-15'
分区裁剪:
├── 2024-01-08/ [跳过]
├── 2024-01-09/ [跳过]
├── 2024-01-10/ [扫描]
├── 2024-01-11/ [扫描]
...
├── 2024-01-15/ [扫描]
├── 2024-01-16/ [跳过]
复制策略
写入路径(多副本):
Writer
│
┌────┴────┐
▼ ▼
Node1 Node2
(主) (从)
│ │
└────┬────┘
▼
一致性检查
分片策略
1. 基于度量名分片:
cpu.* → Shard1
memory.* → Shard2
disk.* → Shard3
2. 基于时间分片:
[00:00-08:00) → Shard1
[08:00-16:00) → Shard2
[16:00-24:00) → Shard3
3. 基于标签哈希分片:
hash(tags) % N → ShardID
多模型数据库如ArangoDB、CosmosDB、OrientDB支持在单一系统中使用多种数据模型。这种设计避免了多种专用数据库带来的运维复杂性和数据一致性问题。
多模型数据库的核心是找到不同数据模型的共同抽象:
通用数据表示
基础元素:
┌─────────────────────────────────┐
│ Entity/Document │
│ ┌────────────────────────┐ │
│ │ _id: unique_identifier │ │
│ │ _type: model_type │ │
│ │ properties: {...} │ │
│ │ edges: [...] │ │
│ └────────────────────────┘ │
└─────────────────────────────────┘
模型映射:
- 文档模型: Entity = Document
- 图模型: Entity = Vertex, Edge = Relationship
- 键值模型: Entity = {_id: key, value: ...}
- 关系模型: Entity = Row, Collection = Table
多模型视图
同一数据的不同视图:
原始数据:
{
"_id": "user_123",
"_type": "vertex",
"name": "张三",
"age": 28,
"_edges": [
{"_to": "user_456", "type": "friend"},
{"_to": "product_789", "type": "purchased"}
]
}
文档视图:普通JSON文档
图视图:顶点with边关系
关系视图:User表的一行
多模型数据库需要统一的查询语言支持不同模型:
AQL (ArangoDB Query Language)示例
// 文档查询
FOR doc IN users
FILTER doc.age > 25
RETURN doc
// 图遍历
FOR v, e, p IN 1..3 OUTBOUND 'users/123' friends
RETURN p
// 连接查询
FOR u IN users
FOR o IN orders
FILTER u._id == o.user_id
RETURN {user: u.name, order: o.total}
// 聚合查询
FOR u IN users
COLLECT city = u.address.city INTO groups
RETURN {city: city, count: LENGTH(groups)}
查询优化器架构
查询解析流程:
查询字符串
│
┌────▼────┐
│ Parser │
└────┬────┘
│ AST
┌────▼────┐
│Optimizer│
└────┬────┘
│
┌────▼─────────────┐
│ Model Detector │
└──┬──────┬────┬──┘
│ │ │
文档计划 图计划 关系计划
│ │ │
└──┬───┴────┘
│
┌─────▼─────┐
│ Executor │
└───────────┘
分层存储设计
逻辑层:
┌──────────────────────────────┐
│ 统一API接口 │
├──────┬──────┬──────┬────────┤
│ Doc │Graph │ KV │ Column │
│ API │ API │ API │ API │
└──────┴──────┴──────┴────────┘
│
存储层:
┌──────────────────────────────┐
│ 通用存储引擎 │
├──────────────────────────────┤
│ 文档存储 │ 图索引 │ 列存储 │
├──────────────────────────────┤
│ LSM-Tree / B-Tree │
└──────────────────────────────┘
索引管理
多类型索引:
┌─────────────────────────────┐
│ 索引管理器 │
├─────────────────────────────┤
│ • Hash索引 (键值查询) │
│ • B-Tree索引 (范围查询) │
│ • 全文索引 (文本搜索) │
│ • 地理索引 (空间查询) │
│ • 图索引 (关系遍历) │
└─────────────────────────────┘
索引选择策略:
- 基于查询模式自动推荐索引
- 支持跨模型的复合索引
- 索引维护成本评估
多模型数据库的事务需要跨越不同数据模型:
ACID保证
事务示例(跨模型):
BEGIN TRANSACTION
// 文档操作
INSERT INTO users VALUES {...}
// 图操作
CREATE EDGE friend FROM users/123 TO users/456
// 键值操作
SET cache:user:123 = {...}
COMMIT
事务日志:
┌──────────────────────────────┐
│ TxID: 1001 │
│ Operations: │
│ 1. DOC_INSERT users/789 │
│ 2. EDGE_CREATE friend/101 │
│ 3. KV_SET cache:user:123 │
│ Status: COMMITTED │
└──────────────────────────────┘
隔离级别实现
MVCC实现多版本:
Document版本链:
v1 (t=100) → v2 (t=200) → v3 (t=300)
↑ ↑ ↑
Tx_A读 Tx_B读 当前版本
不同模型的版本管理:
- 文档:完整文档版本
- 图:节点/边属性版本
- 键值:值版本
查询路由
智能路由决策:
查询分析 → 模型识别 → 最优执行路径
示例:
// 查询:找出张三的所有朋友购买的产品
分析结果:
1. 文档查询找到张三 (文档模型)
2. 图遍历找朋友 (图模型)
3. 连接查询找产品 (关系模型)
执行计划:
┌─────────┐ ┌─────────┐ ┌─────────┐
│Doc Query│ --> │Graph BFS│ --> │Join Op │
└─────────┘ └─────────┘ └─────────┘
缓存策略
多级缓存:
┌─────────────────────────────┐
│ 查询结果缓存 │
├─────────────────────────────┤
│ 模型特定缓存 │
│ • 文档缓存 │
│ • 图遍历缓存 │
│ • 聚合结果缓存 │
├─────────────────────────────┤
│ 页面缓存(Buffer Pool) │
└─────────────────────────────┘
缓存失效策略:
- 基于TTL的过期
- 基于LRU的淘汰
- 事务提交时的主动失效
适用场景
性能权衡
专用数据库 vs 多模型数据库:
性能对比:
┌──────────────┬─────────┬──────────┐
│ 操作类型 │ 专用DB │ 多模型DB │
├──────────────┼─────────┼──────────┤
│ 简单KV查询 │ ★★★★★ │ ★★★★ │
│ 复杂图遍历 │ ★★★★★ │ ★★★★ │
│ 时序聚合 │ ★★★★★ │ ★★★ │
│ 跨模型查询 │ ★★ │ ★★★★★ │
│ 运维复杂度 │ ★★ │ ★★★★★ │
└──────────────┴─────────┴──────────┘
NoSQL数据库的出现并非要取代关系型数据库,而是为特定场景提供更优化的解决方案。本章深入探讨了五种主要的NoSQL数据库类型及其设计原理:
Bloom Filter假阳性率: \(p ≈ (1 - e^{-kn/m})^k\) 其中 $m$ 是位数组大小,$n$ 是元素数量,$k$ 是哈希函数个数
LSM树写入放大: \(WA = \frac{层数 × 放大因子}{2}\) 典型值:10层 × 10倍 / 2 = 50倍
选择NoSQL数据库的决策流程:
数据特征分析
│
├─ 数据是否有固定模式?
│ ├─ 否 → 文档数据库
│ └─ 是 ↓
│
├─ 关系是否是核心?
│ ├─ 是 → 图数据库
│ └─ 否 ↓
│
├─ 是否时间序列数据?
│ ├─ 是 → 时序数据库
│ └─ 否 ↓
│
├─ 是否需要极高写入吞吐?
│ ├─ 是 → 列族存储
│ └─ 否 ↓
│
└─ 是否需要多种访问模式?
├─ 是 → 多模型数据库
└─ 否 → 关系型数据库
练习9.1:文档模型设计 设计一个电商平台的用户和订单数据模型。考虑以下需求:
Hint:考虑嵌入vs引用的权衡,原子性边界在哪里?
练习9.2:LSM树Compaction计算 一个列族存储系统使用Leveled Compaction,每层大小比例为10:1。如果:
计算完成一次从Level 0到Level 1的compaction后,最坏情况下的写入放大。
Hint:考虑Level 1可能已经接近满容量。
练习9.3:图遍历复杂度 在一个社交网络图中,平均每个用户有150个朋友。如果要找出某用户的三度人脉(朋友的朋友的朋友),最坏情况下需要访问多少个节点?如何优化?
Hint:考虑重复访问和双向搜索。
练习9.4:时序数据压缩设计 设计一个针对物联网传感器数据的压缩方案。数据特点:
Hint:结合多种压缩技术,考虑分段存储。
练习9.5:多模型查询优化 在一个多模型数据库中执行以下查询: “找出张三的所有朋友在最近30天内购买过的,且评分>4星的商品”
设计最优的执行计划,说明每步使用哪种数据模型。
Hint:考虑选择性和数据局部性。
练习9.6:CAP权衡分析 设计一个全球部署的物联网平台的数据存储方案。需求:
为实时和历史数据分别选择合适的NoSQL方案,说明CAP权衡。
Hint:考虑Lambda架构,不同层级不同权衡。
练习9.7:图分区算法设计 设计一个社交网络图的分区算法,目标是最小化跨分区边。已知:
Hint:考虑多目标优化和启发式方法。
练习9.8:NoSQL迁移方案 设计从MySQL迁移到NoSQL的方案。现有系统:
Hint:考虑双写、数据一致性校验、回滚方案。