前情回顾

前两篇我们看到了数据库世界的两极:

  • 关系型:ACID 事务、SQL 标准、但单机天花板
  • NoSQL:无限扩展、灵活 Schema、但牺牲一致性

这像是一个权衡三角(注意:这不是 CAP 定理,CAP 讨论的是分布式系统中一致性/可用性/分区容错的取舍):

        一致性(C)
          /\
         /  \
        /    \
       /______\
可扩展性(S)  可用性(A)

传统关系型:优先 C 和 A,牺牲 S(单机架构)
传统 NoSQL:优先 S 和 A,牺牲 C(最终一致性)

2012 年,Google 发表了 Spanner 论文,证明了一个惊人的事实:

三者可以兼顾——代价是巨大的工程复杂度。

分布式的核心挑战

在聊解决方案之前,让我们搞清楚问题有多难。

挑战一:数据分片

数据太多,一台机器存不下,怎么办?拆分到多台机器

分片策略:

方案 1:范围分片(Range Sharding)
┌─────────────┬─────────────┬─────────────┐
│   节点 A    │   节点 B     │   节点 C    │
│  id: 1-100  │ id: 101-200 │ id: 201-300│
└─────────────┴─────────────┴─────────────┘
优点:范围查询友好
缺点:热点问题(新数据都写最后一个节点)

方案 2:哈希分片(Hash Sharding)
节点 = hash(id) % 节点数
┌─────────────┬─────────────┬─────────────┐
│   节点 A    │   节点 B     │   节点 C    │
│ hash % 3=0 │ hash % 3=1  │ hash % 3=2  │
└─────────────┴─────────────┴─────────────┘
优点:数据均匀分布
缺点:范围查询要扫所有节点

方案 3:一致性哈希(Consistent Hashing)
优点:加减节点时只迁移部分数据
缺点:实现复杂

看起来不难?问题在于:分片后怎么查询?

-- 用户表按 user_id 分片
-- 订单表按 order_id 分片

SELECT * FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.city = '北京';

-- user_id = 1 在节点 A
-- 这个用户的订单可能在节点 B(因为按 order_id 分片)
-- JOIN 怎么执行?

挑战二:分布式事务

转账场景:A 给 B 转 100 元。

单机事务(简单):
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE user = 'A';
UPDATE accounts SET balance = balance + 100 WHERE user = 'B';
COMMIT;

分布式事务(A 在节点 1,B 在节点 2):
节点 1:扣 A 的钱
节点 2:加 B 的钱

问题:
- 节点 1 成功,节点 2 失败?
- 节点 1 成功,网络断了,节点 2 不知道?
- 两边都"成功",但网络分区导致数据不一致?

经典解决方案:两阶段提交(2PC)

协调者:
┌─────────────────────────────────────────────────────┐
│                   两阶段提交                          │
├─────────────────────────────────────────────────────┤
│                                                     │
│  阶段 1:准备(Prepare)                              │
│  协调者 → 节点 1:能提交吗?                          │
│  协调者 → 节点 2:能提交吗?                          │
│  节点 1 → 协调者:准备好了(锁定资源)                  │
│  节点 2 → 协调者:准备好了(锁定资源)                  │
│                                                     │
│  阶段 2:提交(Commit)                              │
│  协调者 → 节点 1:提交!                              │
│  协调者 → 节点 2:提交!                              │
│  (如果任一节点说"不行",则全部回滚)                   │
│                                                     │
└─────────────────────────────────────────────────────┘

2PC 的问题:

1. 协调者单点故障
   协调者挂了,所有参与者锁住资源等待

2. 阻塞
   准备阶段锁定资源,直到提交
   并发性能差

3. 网络分区
   协调者发了 Commit,但网络断了
   部分节点收到,部分没收到

挑战三:时钟同步

分布式系统没有"全局时钟"。

场景:
时刻 T1:用户 A 在节点 1 写入 x = 1
时刻 T2:用户 B 在节点 2 读取 x

问题:T2 比 T1 晚吗?

节点 1 的时钟:10:00:00.000
节点 2 的时钟:10:00:00.100

时钟偏差 100ms,谁先谁后?

这个问题看似简单,却困扰了分布式系统几十年。

Google Spanner 的解决方案是硬件:原子钟 + GPS,把时钟误差控制在 7ms 以内。

普通公司买不起原子钟怎么办?后面会讲。

分片演进史

让我们看看分布式数据库是怎么一步步进化的。

阶段一:应用层分片

最原始的方案:在应用代码里手动分片。

fn get_db_connection(user_id: i64) -> PgPool {
    let shard = user_id % 4;
    match shard {
        0 => POOL_SHARD_0.clone(),
        1 => POOL_SHARD_1.clone(),
        2 => POOL_SHARD_2.clone(),
        3 => POOL_SHARD_3.clone(),
        _ => unreachable!(),
    }
}

async fn get_user(user_id: i64) -> Result<User, Error> {
    let pool = get_db_connection(user_id);
    sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", user_id)
        .fetch_one(&pool)
        .await
}

问题一大堆

1. 跨分片查询
   "查询所有北京用户" → 要查 4 个库然后合并

2. 跨分片事务
   用户 A(分片 0)给用户 B(分片 2)转账 → 没有事务保证

3. 分片键变更
   当初按 user_id 分片,现在想按 city 分片 → 数据大迁移

4. 扩容
   从 4 个分片变成 8 个 → 所有数据重新分布

阶段二:数据库中间件

把分片逻辑从应用移到中间件。

┌─────────────────────────────────────┐
│            应用程序                  │
│        (以为只有一个数据库)          │
└───────────────┬─────────────────────┘
┌───────────────▼─────────────────────┐
│         中间件(Proxy)              │
│   解析 SQL → 路由到正确的分片         │
└───────────────┬─────────────────────┘
        ┌───────┼───────┐
        ▼       ▼       ▼
    ┌──────┐┌──────┐┌──────┐
    │Shard1││Shard2││Shard3│
    └──────┘└──────┘└──────┘

代表产品:

  • MyCat:MySQL 中间件
  • Vitess:YouTube 开源,MySQL 分片
  • ShardingSphere:Java 生态

中间件解决了"对应用透明"的问题,但:

局限:
1. 跨分片 JOIN 依然很慢(或不支持)
2. 分布式事务依然困难
3. 中间件本身成为瓶颈和单点
4. 运维复杂度高

阶段三:原生分布式(NewSQL)

把分布式能力内置到数据库引擎里。

NewSQL 的目标:

┌─────────────────────────────────────────────┐
│              鱼与熊掌兼得                     │
├─────────────────────────────────────────────┤
│  ✓ 关系模型(表、SQL、JOIN)                 │
│  ✓ ACID 事务(跨节点)                       │
│  ✓ 水平扩展(加节点 = 加容量)               │
│  ✓ 高可用(节点挂了自动恢复)                │
└─────────────────────────────────────────────┘

Google Spanner:NewSQL 的开创者

2012 年,Google 发布 Spanner 论文,震惊业界。

Spanner 的创新:

1. TrueTime API
   用原子钟 + GPS 实现全球时钟同步
   误差 < 7ms

2. 外部一致性
   比传统强一致性更强
   全球任意位置读到的数据一致

3. 自动分片和 Rebalance
   数据自动分布,热点自动迁移

4. Paxos 复制
   每个数据至少 3 副本
   自动选主、故障转移

Spanner 证明了:分布式事务可以做到全球规模

但 Spanner 是 Google 内部服务,普通公司用不了。

于是开源社区开始复制它。

CockroachDB:开源的 Spanner

CockroachDB(蟑螂数据库)的目标:做开源版 Spanner。

CockroachDB 特性:

- 兼容 PostgreSQL 协议(现有工具可用)
- 分布式 ACID 事务
- 自动分片、自动 Rebalance
- 多副本、自动故障转移
- 不需要原子钟(用混合逻辑时钟 HLC)

架构概览

┌─────────────────────────────────────────────────────┐
│                  CockroachDB 集群                    │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ┌─────────┐    ┌─────────┐    ┌─────────┐        │
│  │ Node 1  │    │ Node 2  │    │ Node 3  │        │
│  │┌───────┐│    │┌───────┐│    │┌───────┐│        │
│  ││Range A││    ││Range A││    ││Range A││ 副本    │
│  │└───────┘│    │└───────┘│    │└───────┘│        │
│  │┌───────┐│    │┌───────┐│    │┌───────┐│        │
│  ││Range B││    ││Range C││    ││Range B││        │
│  │└───────┘│    │└───────┘│    │└───────┘│        │
│  └─────────┘    └─────────┘    └─────────┘        │
│                                                     │
│  数据自动分成 Range(默认 512MB)                    │
│  每个 Range 3 副本,分布在不同节点                    │
│  Raft 协议保证副本一致性                             │
│                                                     │
└─────────────────────────────────────────────────────┘

TiDB:兼容 MySQL 的分布式

TiDB(PingCAP 开发)选择了另一条路:兼容 MySQL

TiDB 架构:

┌─────────────────────────────────────────────────────┐
│                     TiDB 集群                        │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ┌──────────────────────────────────────────────┐   │
│  │              TiDB Server(无状态)             │   │
│  │         SQL 解析、优化、执行                   │   │
│  └──────────────────────────────────────────────┘   │
│                         │                           │
│  ┌──────────────────────▼──────────────────────┐   │
│  │                 TiKV(存储层)                │   │
│  │    分布式 KV 存储,Raft 复制,MVCC            │   │
│  │  ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐   │   │
│  │  │Store1│  │Store2│  │Store3│  │Store4│   │   │
│  │  └──────┘  └──────┘  └──────┘  └──────┘   │   │
│  └─────────────────────────────────────────────┘   │
│                                                     │
│  ┌──────────────────────────────────────────────┐   │
│  │              PD(调度中心)                    │   │
│  │         元数据管理、负载均衡、调度              │   │
│  └──────────────────────────────────────────────┘   │
│                                                     │
└─────────────────────────────────────────────────────┘

TiDB 的杀手锏

1. MySQL 兼容
   - 现有 MySQL 应用几乎不改代码
   - MySQL 工具链可用(mysqldump、客户端等)

2. HTAP(混合负载)
   - TiKV:行存储,适合 OLTP
   - TiFlash:列存储,适合 OLAP
   - 同一集群同时支持交易和分析

3. 生态
   - 中国公司开发,中文文档完善
   - 国内用户多,社区活跃

PostgreSQL 的分布式之路:Citus

PostgreSQL 不甘落后,Citus 是它的分布式扩展。

Citus 架构:

┌─────────────────────────────────────────────────────┐
│                   Citus 集群                         │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ┌─────────────────────────────────────────────┐    │
│  │           Coordinator(协调节点)             │    │
│  │    接收 SQL → 分发到 Worker → 合并结果        │    │
│  └─────────────────────────────────────────────┘    │
│            │              │              │          │
│     ┌──────▼──────┐┌──────▼──────┐┌──────▼──────┐  │
│     │  Worker 1   ││  Worker 2   ││  Worker 3   │  │
│     │ (PostgreSQL)││ (PostgreSQL)││ (PostgreSQL)│  │
│     │┌───────────┐││┌───────────┐││┌───────────┐│  │
│     ││ Shard 1,4 ││││ Shard 2,5 ││││ Shard 3,6 ││  │
│     │└───────────┘││└───────────┘││└───────────┘│  │
│     └─────────────┘└─────────────┘└─────────────┘  │
│                                                     │
└─────────────────────────────────────────────────────┘

Citus 的独特之处

-- Citus 是 PostgreSQL 扩展,不是独立产品
CREATE EXTENSION citus;

-- 创建分布式表
CREATE TABLE events (
    id BIGSERIAL,
    tenant_id INT NOT NULL,
    event_type VARCHAR(50),
    payload JSONB,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 按 tenant_id 分片(多租户场景)
SELECT create_distributed_table('events', 'tenant_id');

-- 创建参考表(小表,每个节点一份)
CREATE TABLE event_types (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50)
);
SELECT create_reference_table('event_types');

-- 之后就是普通 SQL,Citus 自动处理分布式
SELECT tenant_id, COUNT(*)
FROM events
WHERE created_at > NOW() - INTERVAL '1 day'
GROUP BY tenant_id;

Citus 的适用场景

非常适合:
- 多租户 SaaS(按 tenant_id 分片)
- 实时分析(大量聚合查询)
- 已有 PostgreSQL 技术栈

不太适合:
- 跨分片事务频繁
- 分片键不明确
- 需要全球多活

选型指南

┌─────────────────────────────────────────────────────────────────┐
│                    分布式数据库选型                              │
├───────────────┬─────────────────────────────────────────────────┤
│     产品       │              适用场景                           │
├───────────────┼─────────────────────────────────────────────────┤
│ CockroachDB   │ 全球部署、强一致性要求、兼容 PostgreSQL          │
│ TiDB          │ MySQL 生态、HTAP 混合负载、国内团队              │
│ Citus         │ 多租户 SaaS、已有 PostgreSQL、实时分析          │
│ Vitess        │ MySQL 分片、超大规模、YouTube 同款              │
│ YugabyteDB    │ 兼容 PostgreSQL + Cassandra API                │
└───────────────┴─────────────────────────────────────────────────┘

决策流程

需要分布式吗?
├─ 数据量 < 1TB,QPS < 10K → 单机 PostgreSQL 够了
└─ 真的需要 →
    现有技术栈?
    ├─ MySQL → TiDB 或 Vitess
    └─ PostgreSQL → Citus 或 CockroachDB
        场景特点?
        ├─ 多租户 SaaS → Citus
        ├─ 全球部署 → CockroachDB
        └─ HTAP → TiDB(TiFlash)

分布式事务实战

以 Citus 为例,展示分布式事务:

use sqlx::{PgPool, postgres::PgPoolOptions};
use rust_decimal::Decimal;

async fn transfer_between_tenants(
    pool: &PgPool,
    from_tenant: i32,
    to_tenant: i32,
    amount: Decimal,
) -> Result<(), sqlx::Error> {
    // Citus 支持跨分片事务(2PC)
    let mut tx = pool.begin().await?;

    // 扣款(可能在节点 A)
    sqlx::query!(
        r#"
        UPDATE accounts
        SET balance = balance - $1
        WHERE tenant_id = $2
        "#,
        amount,
        from_tenant
    )
    .execute(&mut *tx)
    .await?;

    // 入账(可能在节点 B)
    sqlx::query!(
        r#"
        UPDATE accounts
        SET balance = balance + $1
        WHERE tenant_id = $2
        "#,
        amount,
        to_tenant
    )
    .execute(&mut *tx)
    .await?;

    // Citus 会用 2PC 保证原子性
    tx.commit().await?;

    Ok(())
}

注意:跨分片事务比单分片事务慢很多,设计时尽量让相关数据在同一分片。

核心认知

分布式数据库的本质:用工程复杂度换取无限扩展能力,同时尽量保留关系型的优点。

NewSQL 教会我们

  • CAP 不是借口,工程可以突破理论限制
  • 分布式事务可以做到,但有性能代价
  • 分片键设计是成败关键
  • 大多数系统不需要分布式数据库

什么时候真的需要分布式

  • 单表 > 10 亿行
  • 单机 IO/CPU 持续打满
  • 需要跨地域部署
  • 需要极高可用(RPO=0)

引出下一篇

到目前为止,我们讨论的数据库都有一个共同假设:数据是用来做交易的(OLTP)——增删改查、事务处理、实时响应。

但还有另一类需求:把数据拿来分析(OLAP)——聚合、统计、报表、商业智能。

同样的数据,不同的使用方式,需要完全不同的优化策略。当你需要"过去一年每个地区每类产品的销售趋势"时,传统数据库就显得力不从心了。

下一篇,我们来看:分析型数据库——当查询变成分析


常见问题

Q:我的系统需要分布式数据库吗?

A:大概率不需要。

常见误解:
"我们用户增长很快,要提前考虑扩展性"
"大厂都用分布式,我们也要用"

现实:
- PostgreSQL 单机可以轻松处理每秒数万 QPS
- 读写分离 + 连接池可以撑很久
- 真正需要分布式的公司不到 1%

先问自己:
1. 当前 PostgreSQL 遇到性能瓶颈了吗?
2. 瓶颈是 CPU、内存、磁盘 IO 还是网络?
3. 分区表 + 读写分离试过了吗?

Q:TiDB 和 CockroachDB 选哪个?

A:看你的技术栈和团队。

维度TiDBCockroachDB
协议兼容MySQLPostgreSQL
公司背景PingCAP(中国)Cockroach Labs(美国)
文档语言中文为主英文为主
HTAPTiFlash 列存暂无
部署复杂度较高(多组件)较低(单二进制)

Q:分布式数据库的运维难度如何?

A:比单机复杂得多。

需要考虑:
- 节点扩缩容
- 数据 Rebalance
- 故障检测与恢复
- 跨节点查询调优
- 备份恢复策略
- 版本升级(滚动升级)

建议:
- 有专职 DBA
- 或者用云服务(TiDB Cloud、CockroachDB Cloud、Azure Citus)

上一篇:嵌入式数据库——藏在你身边的数据库

下一篇:分析型数据库——当查询变成分析

本系列:

  1. 关系型数据库——SQL 一统江湖的时代
  2. NoSQL 运动——打破关系的枷锁
  3. 嵌入式数据库——藏在你身边的数据库
  4. 分布式数据库——当单机不够用(本篇)
  5. 分析型数据库——当查询变成分析
  6. 时序数据库——时间维度的专家
  7. 向量数据库——AI 时代的新基建
  8. PostgreSQL——一个数据库的文艺复兴