前情回顾
前两篇我们看到了数据库世界的两极:
- 关系型: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:看你的技术栈和团队。
| 维度 | TiDB | CockroachDB |
|---|---|---|
| 协议兼容 | MySQL | PostgreSQL |
| 公司背景 | PingCAP(中国) | Cockroach Labs(美国) |
| 文档语言 | 中文为主 | 英文为主 |
| HTAP | TiFlash 列存 | 暂无 |
| 部署复杂度 | 较高(多组件) | 较低(单二进制) |
Q:分布式数据库的运维难度如何?
A:比单机复杂得多。
需要考虑:
- 节点扩缩容
- 数据 Rebalance
- 故障检测与恢复
- 跨节点查询调优
- 备份恢复策略
- 版本升级(滚动升级)
建议:
- 有专职 DBA
- 或者用云服务(TiDB Cloud、CockroachDB Cloud、Azure Citus)
下一篇:分析型数据库——当查询变成分析
本系列: