跳转至

💾 数据存储设计

⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。

系统设计数据存储架构图

学习时间:6小时 | 难度:⭐⭐⭐ 中等 | 前置知识:数据库基础、02-核心组件详解


🎯 本章目标

  • 掌握数据库分片策略(Range/Hash/一致性哈希)
  • 理解分库分表的完整方案设计
  • 掌握读写分离架构和主从复制模式
  • 深入理解分布式事务的各种解决方案
  • 了解NewSQL数据库的特性

📋 目录


1. 数据库分片

1.1 为什么需要分片

Text Only
单机数据库瓶颈:
  - 存储瓶颈:单机磁盘有限(如2TB)
  - 性能瓶颈:单机QPS有上限(如5000 QPS)
  - 可用性:单点故障风险

分片解决方案:
  将数据分散到多台数据库服务器上

  分片前:                    分片后:
  ┌──────────┐              ┌──────┐ ┌──────┐ ┌──────┐
  │ 1亿条数据 │              │ 3300 │ │ 3300 │ │ 3400 │
  │ 单机扛不住 │      →       │ 万条  │ │ 万条  │ │ 万条  │
  └──────────┘              │Shard1│ │Shard2│ │Shard3│
                            └──────┘ └──────┘ └──────┘

1.2 Range分片(范围分片)

Text Only
按范围划分数据:

  user_id: 1~1000万     → Shard 1
  user_id: 1000万~2000万 → Shard 2
  user_id: 2000万~3000万 → Shard 3

  ┌─────────────────────────────────────────┐
  │           user_id 范围                    │
  ├──────────┬────────────┬─────────────────┤
  │  1~10M   │  10M~20M   │   20M~30M       │
  │ Shard 1  │  Shard 2   │   Shard 3       │
  └──────────┴────────────┴─────────────────┘
优势 劣势
范围查询高效 数据分布不均(热点问题)
实现简单 新用户都集中在最后一个分片
扩展方便(加新范围) 需要监控各分片数据量

适用场景:时间序列数据(按月份分片)、自增ID且查询有范围条件

1.3 Hash分片(哈希分片)

Text Only
按哈希值划分数据:

  shard = hash(user_id) % 分片数

  hash(user_1) % 3 = 0  → Shard 0
  hash(user_2) % 3 = 1  → Shard 1
  hash(user_3) % 3 = 2  → Shard 2
  hash(user_4) % 3 = 0  → Shard 0

  ┌──────────┐   hash(key)%3   ┌────────┐
  │  请求     │ ──────────────→ │ 0: S0  │
  │ key=xxx  │                 │ 1: S1  │
  └──────────┘                 │ 2: S2  │
                               └────────┘
优势 劣势
数据分布均匀 范围查询需要查所有分片
避免热点 扩容时需要数据迁移
实现简单 hash(key) % N, N变化影响大

适用场景:数据量大、主要按主键查询

1.4 一致性哈希分片

解决普通Hash分片在扩容时大量数据迁移的问题:

Text Only
普通Hash:hash % 3 → hash % 4
  几乎所有数据都要重新分配!

一致性哈希:增加一个节点
  只需要迁移 1/N 的数据

  哈希环:
                    0
                  ╱   ╲
                S1      ╲
               ╱          S2
              │     +S_new │  ← 新增节点
              │            │
               ╲          ╱
                S3      ╱
                  ╲   ╱
                    (bottom)

  只有S_new和下一个节点之间的数据需要迁移

1.5 分片策略对比

策略 数据均匀性 范围查询 扩容成本 实现复杂度
Range ❌ 不均匀 ✅ 高效 ✅ 简单
Hash ✅ 均匀 ❌ 全扫描 ❌ 大量迁移
一致性Hash ✅ 较均匀 ❌ 全扫描 ✅ 少量迁移
复合分片 ✅ 均匀 ✅ 部分支持

2. 分库分表

2.1 垂直拆分 vs 水平拆分

Text Only
垂直拆分(按业务/字段拆分):

  拆分前:                     拆分后:
  ┌────────────────────┐      ┌──────────┐  ┌──────────┐
  │ user_id            │      │ user_id  │  │ user_id  │
  │ username           │  →   │ username │  │ avatar   │
  │ email              │      │ email    │  │ bio      │
  │ avatar(大字段)     │      │ (核心表)  │  │ (扩展表)  │
  │ bio(大字段)        │      └──────────┘  └──────────┘
  └────────────────────┘

水平拆分(按行拆分):

  拆分前:                     拆分后:
  ┌──────────────────┐        ┌──────────┐  ┌──────────┐
  │ 1亿行 orders      │  →    │ 5000万行  │  │ 5000万行  │
  │                   │       │ orders_0 │  │ orders_1 │
  └──────────────────┘        └──────────┘  └──────────┘

2.2 垂直拆分详解

垂直分库(按业务拆分):

Text Only
单体数据库:                     拆分后:
┌──────────────────┐           ┌────────┐  ┌────────┐  ┌────────┐
│ users表          │           │用户库   │  │订单库   │  │商品库   │
│ orders表         │   →       │ users  │  │ orders │  │products│
│ products表       │           │ addrs  │  │ items  │  │ stocks │
│ addresses表      │           └────────┘  └────────┘  └────────┘
│ order_items表    │
│ stocks表         │
└──────────────────┘

垂直分表(按字段拆分):

Text Only
适用:表中有大字段且访问频率不同

users表:
  user_id, name, email, phone  ← 高频访问
  avatar, bio, preferences     ← 低频访问

拆分为:
  users_base (user_id, name, email, phone)  ← 热数据
  users_ext  (user_id, avatar, bio, prefs)  ← 冷数据

2.3 水平拆分详解

分片键选择是水平拆分最关键的决策:

分片键要求 说明 示例
高区分度 值的分布要均匀 ✅ user_id, ❌ gender
查询关联 大部分查询都会带上这个字段 订单表用user_id
业务关联 同一业务的数据在同一分片 同一用户的订单在同一分片
不可变 分片键不应该修改 ✅ user_id, ❌ status

分片键选择示例

Text Only
场景:电商订单表

方案1: 按 order_id 分片
  ✅ 按订单号查询快
  ❌ 查某用户所有订单需要全分片扫描

方案2: 按 user_id 分片
  ✅ 查某用户所有订单只需查一个分片
  ✅ 用户相关的订单都在一起
  ❌ 大卖家的订单可能集中

方案3: 按 user_id 分片 + order_id 包含user_id信息
  ✅ 两种查询都高效
  order_id = user_id(高位) + 时间戳 + 序列号

2.4 分库分表中间件

中间件 类型 开发商 特点
ShardingSphere 客户端/代理 Apache 功能全面,生态好
Vitess 代理 YouTube/CNCF MySQL专用,K8s原生
MyCat 代理 社区 国内流行,已较少维护
DBLE 代理 爱可生 MyCat的企业级分支

2.5 分库分表后的问题

问题 说明 解决方案
跨分片查询 查询涉及多个分片 冗余数据/全局表/查询引擎
跨分片Join 无法直接Join两个分片 应用层Join/宽表冗余
跨分片事务 分布式事务 柔性事务(Saga/TCC)
全局排序 ORDER BY + LIMIT 各分片排序后归并
全局ID 自增ID冲突 Snowflake等分布式ID
扩容 增加分片 一致性哈希/成倍扩容
Text Only
跨分片排序方案:

查询: SELECT * FROM orders ORDER BY created_at DESC LIMIT 10

Shard 0: TOP 10 orders (sorted) → [o1, o3, o5, ...]
Shard 1: TOP 10 orders (sorted) → [o2, o4, o6, ...]
Shard 2: TOP 10 orders (sorted) → [o7, o8, o9, ...]
                                    归并排序
                            取所有结果中TOP 10

3. 读写分离

3.1 读写分离架构

Text Only
                    ┌──────────┐
                    │ 应用服务器 │
                    └──┬───┬───┘
                       │   │
                 写请求│   │读请求
                       │   │
                  ┌────▼┐ ┌▼────┐
                  │Master│ │Proxy│ ← 读写分离中间件
                  │(主库)│ │     │
                  └──┬───┘ └──┬──┘
                     │        │
               ┌─────┼────────┼─────┐
               │     │        │     │
          ┌────▼┐ ┌──▼───┐ ┌─▼────┐│
          │Slave│ │Slave │ │Slave ││
          │ 1   │ │  2   │ │  3   ││
          └─────┘ └──────┘ └──────┘│
               读请求分发到从库      │
               └────────────────────┘

3.2 读写分离方案对比

方案 实现方式 优势 劣势
应用层 代码中判断读写路由 灵活、无额外组件 侵入业务代码
中间件 ProxySQL/MaxScale 透明、统一管理 增加一跳延迟
框架层 Spring的AbstractRoutingDataSource 半透明 框架绑定

3.3 主从延迟问题

Text Only
写请求时间线:
  T1: 写入Master
  T2: Master生成binlog
  T3: Slave接收binlog
  T4: Slave重放binlog(replay)
  T5: Slave数据可读

主从延迟 = T5 - T1(通常 ms ~ s级)

问题场景:
  用户注册 → 写Master → 立即查询 → 读Slave → 数据还没同步过来!
  用户以为注册失败了!

解决方案

方案 说明 适用场景
强制读主 关键读操作走主库 写后立即读的场景
延迟感知 判断从库延迟,超阈值走主库 一般场景
会话一致性 同一会话内写后读走主库 用户操作场景
半同步复制 至少一个从库确认才返回 数据一致性要求高
中间件感知 ProxySQL自动判断延迟 通用方案
Python
class ReadWriteRouter:
    """读写路由器 - 解决主从延迟"""

    def __init__(self, master, slaves):
        self.master = master
        self.slaves = slaves
        self._force_master_keys = set()  # 需要强制走主库的key

    def write(self, key, value):
        self.master.write(key, value)
        # 标记这个key在短时间内需要走主库读
        self._force_master_keys.add(key)
        # 延迟清除(如2秒后清除)
        self._schedule_remove(key, delay=2.0)

    def read(self, key):
        if key in self._force_master_keys:
            return self.master.read(key)  # 走主库
        return self._get_slave().read(key)  # 走从库

    def _get_slave(self):
        # 选择延迟最小的从库
        return min(self.slaves, key=lambda s: s.replication_lag())  # lambda匿名函数:简洁的单行函数

4. 主从复制

4.1 复制模式对比

Text Only
异步复制(Async):
  Master: WRITE → ACK(客户端) → 发送binlog给Slave
  Slave:                         接收 → 重放

  ✅ 写性能最高
  ❌ Master挂了可能丢数据

半同步复制(Semi-Sync):
  Master: WRITE → 发送binlog → 至少1个Slave确认 → ACK(客户端)
  Slave:           接收binlog → ACK(Master)

  ✅ 至少1个Slave有数据
  ❌ 写性能下降(等待Slave确认)

全同步复制(Sync):
  Master: WRITE → 发送binlog → 所有Slave确认 → ACK(客户端)
  Slave:           接收binlog → 重放 → ACK(Master)

  ✅ 数据绝对不丢
  ❌ 写性能最差,一个Slave慢就全部慢

4.2 复制模式对比表

特性 异步复制 半同步复制 全同步复制
写延迟 最低 中等 最高
数据安全 可能丢数据 至少1副本 不丢数据
可用性 最高 Slave故障影响写
一致性 最终一致 较强 强一致
适用场景 日志、统计 通用业务 金融交易
MySQL支持 默认模式 插件支持 Group Replication

4.3 复制拓扑

Text Only
主从复制:                    链式复制:
  ┌──────┐                    ┌──────┐
  │Master│                    │Master│
  └──┬───┘                    └──┬───┘
     │                           │
  ┌──┼──┐                    ┌──▼───┐
  │  │  │                    │Slave1│
  ▼  ▼  ▼                    └──┬───┘
 S1 S2 S3                       │
                             ┌──▼───┐
                             │Slave2│
                             └──┬───┘
                             ┌──▼───┐
                             │Slave3│
                             └──────┘

主主复制(双写):              树形复制:
  ┌──────┐  ⟷  ┌──────┐      ┌──────┐
  │Master│     │Master│      │Master│
  │  A   │     │  B   │      └──┬───┘
  └──────┘     └──────┘         │
  互相同步                   ┌───┼───┐
                            S1  S2  S3
                            │       │
                           S4      S5

4.4 故障切换

Text Only
Master故障切换流程(自动):

1. 检测Master故障
   ├── 心跳超时
   └── 连续多次检测失败

2. 选举新Master
   ├── 选择数据最新的Slave
   └── 检查复制位点

3. 提升Slave为新Master
   ├── STOP SLAVE
   ├── RESET SLAVE ALL
   └── 开启写入

4. 其他Slave指向新Master
   └── CHANGE MASTER TO new_master

5. 应用更新连接
   ├── DNS切换
   ├── VIP(Virtual IP)漂移
   └── 或Service Discovery更新

常用工具:
  - MySQL: MHA(Master High Availability)
  - MySQL: Orchestrator
  - MySQL: ProxySQL
  - PostgreSQL: Patroni

5. 数据一致性与分布式事务

5.1 分布式事务问题

Text Only
场景:电商下单
  1. 订单服务:创建订单        → 订单DB
  2. 库存服务:扣减库存        → 库存DB
  3. 积分服务:增加积分        → 积分DB

如果步骤2失败,如何回滚步骤1?
如果步骤2成功但步骤3失败?

这就是分布式事务问题!

5.2 2PC(两阶段提交)

Text Only
                 ┌──────────┐
                 │协调者(TC) │
                 └──┬───┬───┘
                    │   │
Phase 1: 准备      │   │
(Prepare)         │   │
                    │   │
              ┌─────▼┐ ┌▼─────┐
              │参与者A│ │参与者B│
              │Prepare│ │Prepare│
              │ OK?   │ │ OK?  │
              └──┬────┘ └──┬───┘
                 │ YES     │ YES
                 │         │
Phase 2: 提交    ▼         ▼
(Commit)   ┌──────────┐
           │ TC: 都OK │
           │→ COMMIT  │
           └──┬───┬───┘
              │   │
        ┌─────▼┐ ┌▼─────┐
        │COMMIT│ │COMMIT│
        │  A   │ │  B   │
        └──────┘ └──────┘
优势 劣势
强一致性保证 同步阻塞,性能差
实现相对简单 协调者单点问题
数据库原生支持(XA) 网络分区时可能阻塞

5.3 3PC(三阶段提交)

Text Only
Phase 1: CanCommit(询问)
  TC → 参与者: "你能提交吗?"
  参与者 → TC: "能/不能"

Phase 2: PreCommit(预提交)
  TC → 参与者: "准备提交"
  参与者: 进行预提交(但不真正提交)
  参与者 → TC: "预提交完成"

Phase 3: DoCommit(正式提交)
  TC → 参与者: "正式提交"
  参与者: 提交事务

相比2PC的改进: - 增加了CanCommit阶段,减少资源锁定时间 - 参与者引入超时机制,避免无限等待 - 但仍然无法完全解决网络分区问题

5.4 Saga模式

Text Only
Saga = 一系列本地事务 + 补偿操作

正向操作:
  T1(创建订单) → T2(扣减库存) → T3(扣款) → T4(通知)

如果T3失败:
  C2(恢复库存) → C1(取消订单)

  ┌────┐   ┌────┐   ┌────┐   ┌────┐
  │ T1 │ → │ T2 │ → │ T3 │ → │ T4 │ → 成功!
  └────┘   └────┘   └──┬─┘   └────┘
                       │ 失败
                    ┌────┐   ┌────┐
                    │ C2 │ → │ C1 │ → 回滚完成
                    └────┘   └────┘

Saga的两种实现

实现 说明 优势 劣势
编排式 (Choreography) 各服务监听事件自行处理 简单、松耦合 难以追踪、循环依赖
协调式 (Orchestration) 中心协调器控制流程 流程清晰、易管理 协调器复杂、单点风险
Python
# Saga协调器示例
class SagaOrchestrator:
    def __init__(self):
        self.steps = []  # [(forward_action, compensate_action)]
        self.completed_steps = []

    def add_step(self, forward, compensate):
        self.steps.append((forward, compensate))

    def execute(self, context):
        for i, (forward, compensate) in enumerate(self.steps):  # enumerate同时获取索引和值
            try:  # try/except捕获异常
                result = forward(context)
                self.completed_steps.append((compensate, context))
                context.update(result)
            except Exception as e:
                print(f"Step {i} failed: {e}")
                self._compensate()
                raise

    def _compensate(self):
        """按逆序执行补偿操作"""
        for compensate, context in reversed(self.completed_steps):
            try:
                compensate(context)
            except Exception as e:
                print(f"Compensation failed: {e}")
                # 记录日志,人工处理

5.5 TCC(Try-Confirm-Cancel)

Text Only
TCC三个阶段:

Try(尝试):
  - 预留资源
  - 冻结库存(不是真正扣减)
  - 冻结余额(不是真正扣款)

Confirm(确认):
  - 确认提交
  - 冻结库存 → 真正扣减
  - 冻结余额 → 真正扣款

Cancel(取消):
  - 释放预留资源
  - 解冻库存
  - 解冻余额

示例 - 电商下单:
  ┌─────────┐    ┌─────────┐    ┌─────────┐
  │ 订单服务 │    │ 库存服务 │    │ 支付服务 │
  └────┬────┘    └────┬────┘    └────┬────┘
       │              │              │
Try:   │  创建待确认订单│  冻结库存     │  冻结余额
       │              │              │
       ▼              ▼              ▼
       ────── 全部Try成功?──────
              │            │
             是            否
              │            │
Confirm:      │     Cancel: │
  确认订单    │     取消订单 │
  扣减库存    │     解冻库存 │
  扣减余额    │     解冻余额 │
Java
// TCC接口定义
public interface TccService<T> {  // interface定义类型契约
    // Try: 预留资源
    boolean tryAction(T context);

    // Confirm: 确认提交
    boolean confirm(T context);

    // Cancel: 取消释放
    boolean cancel(T context);
}

// 库存TCC实现
public class InventoryTccService implements TccService<OrderContext> {  // extends继承;implements实现接口

    @Override  // @Override重写父类方法
    public boolean tryAction(OrderContext ctx) {
        // 冻结库存(available_stock -= quantity, frozen_stock += quantity)
        return inventoryDao.freezeStock(ctx.getProductId(), ctx.getQuantity());
    }

    @Override
    public boolean confirm(OrderContext ctx) {
        // 扣减冻结库存(frozen_stock -= quantity)
        return inventoryDao.deductFrozenStock(ctx.getProductId(), ctx.getQuantity());
    }

    @Override
    public boolean cancel(OrderContext ctx) {
        // 解冻库存(frozen_stock -= quantity, available_stock += quantity)
        return inventoryDao.unfreezeStock(ctx.getProductId(), ctx.getQuantity());
    }
}

5.6 分布式事务方案对比

方案 一致性 性能 复杂度 适用场景 代表框架
2PC/XA 强一致 数据库层面 MySQL XA
3PC 强一致 理论为主 少有实现
Saga 最终一致 长事务、跨服务 Seata Saga
TCC 最终一致 短事务、金融 Seata TCC
本地消息表 最终一致 异步场景 自定义
事务消息 最终一致 MQ场景 RocketMQ

5.7 本地消息表方案

Text Only
适用:不需要实时一致的场景

流程:
1. 业务操作 + 写消息表(在同一个本地事务中)
2. 定时任务扫描消息表,发送消息
3. 消费者处理消息
4. 处理成功后更新消息状态

  ┌──────────────┐
  │  订单服务      │
  │ ┌──────────┐ │    ┌──────────┐
  │ │ orders   │ │    │ 定时任务  │──→ MQ ──→ 库存服务
  │ │ messages │ │ ←─ │ 扫描未发送│
  │ └──────────┘ │    │ 的消息   │
  └──────────────┘    └──────────┘

关键SQL:
  BEGIN;
    INSERT INTO orders (...) VALUES (...);
    INSERT INTO messages (topic, body, status) VALUES ('order_created', '{}', 'PENDING');
  COMMIT;

6. NewSQL数据库

6.1 NewSQL的定位

Text Only
传统关系数据库        NoSQL              NewSQL
┌──────────┐    ┌──────────┐    ┌──────────────┐
│ ✅ ACID   │    │ ✅ 扩展性  │    │ ✅ ACID      │
│ ✅ SQL    │    │ ✅ 高性能  │    │ ✅ SQL       │
│ ❌ 扩展性  │    │ ❌ 无ACID  │    │ ✅ 扩展性     │
│ ❌ 分布式  │    │ ❌ 弱SQL   │    │ ✅ 分布式     │
└──────────┘    └──────────┘    └──────────────┘
                                 = SQL + NoSQL的优势

6.2 TiDB

Text Only
TiDB架构:

  ┌──────────────────────────────────┐
  │            TiDB Server            │  ← SQL层(无状态,可水平扩展)
  │    SQL解析 → 优化 → 执行          │
  └──────────────┬───────────────────┘
  ┌──────────────▼───────────────────┐
  │              PD                   │  ← 调度层(元数据 + 调度)
  │    全局时钟 + Region调度           │
  └──────────────┬───────────────────┘
  ┌──────────────▼───────────────────┐
  │            TiKV                   │  ← 存储层(分布式KV)
  │  ┌────────┐ ┌────────┐ ┌────────┐│
  │  │Region 1│ │Region 2│ │Region 3││  ← 每个Region是Raft Group
  │  │(3副本) │ │(3副本) │ │(3副本) ││
  │  └────────┘ └────────┘ └────────┘│
  └──────────────────────────────────┘

TiDB特点

特性 说明
MySQL兼容 MySQL协议兼容,迁移成本低
水平扩展 存储和计算分离,独立扩展
强一致 Raft协议保证数据一致性
HTAP 同时支持OLTP和OLAP
在线DDL 不锁表的Schema变更

6.3 CockroachDB

特性 说明
PostgreSQL兼容 PG协议兼容
全球部署 内置跨地域复制
Serializable 默认最高隔离级别
自动分片 透明的Range-based分片

6.4 NewSQL vs 传统分库分表

维度 分库分表 NewSQL(TiDB)
运维复杂度 高(多个MySQL实例) 中(统一管理)
SQL兼容 有限(跨分片查询受限) 完全兼容
分布式事务 需要额外方案 内置支持
扩容 需要数据迁移 自动均衡
成熟度 非常成熟 相对较新
性能 单分片查询快 略低(分布式开销)
适用规模 中大型 大型

7. 练习与延伸阅读

7.1 练习题

  1. 分片设计:为一个DAU 1亿的社交应用设计消息表的分片方案
  2. 考虑:用户间消息、群聊消息、消息搜索

  3. 读写分离:设计一个支持"写后立即读"的读写分离方案

  4. 分布式事务:为电商退款场景设计Saga方案

  5. 涉及:订单服务、库存服务、支付服务、积分服务

  6. 容量规划

  7. 某电商平台日均500万订单
  8. 每条订单约2KB
  9. 设计分库分表方案(考虑数据增长5年)

  10. 方案选型:对比分析以下场景应该用分库分表还是NewSQL:

  11. 场景A:已有大量MySQL存储的电商系统
  12. 场景B:全新的金融交易系统

7.2 延伸阅读

  • 《DDIA》第5章 — 复制
  • 《DDIA》第6章 — 分区
  • 《DDIA》第7章 — 事务
  • TiDB官方文档 — 架构设计
  • Seata官方文档 — 分布式事务方案
  • 美团技术博客 — "大众点评订单系统分库分表实践"
  • 阿里技术博客 — "分布式事务解决方案"

📝 本章小结

知识点 关键要点
分片策略 Range适合范围查询、Hash分布均匀、一致性哈希扩容友好
分库分表 垂直按业务/字段拆分、水平按行拆分、分片键选择是核心
读写分离 缓解读压力、主从延迟是核心问题
主从复制 异步性能好但可能丢数据、半同步是折中
分布式事务 2PC强一致但性能差、Saga/TCC最终一致但灵活
NewSQL TiDB兼容MySQL + 分布式,是分库分表的替代方案

上一章:核心组件详解 | 下一章:分布式系统基础