mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
4013 字
11 分钟
分库分表与 NewSQL:Sharding 策略与分布式数据库
2024-11-02

当单表数据量突破千万、写入 QPS 打满单机、慢查询越来越多——你开始认真考虑分库分表。但分库分表不是银弹:跨分片 Join 怎么办?分布式事务怎么保证?分页排序怎么做?与此同时,NewSQL 数据库声称能”像分库分表一样扩展,像单机数据库一样使用”——它们做到了吗?

本章从分库分表的工程实践出发,逐层拆解拆分策略、分片键选择、分布式 ID、跨分片查询与分布式事务,再深入 NewSQL 数据库的架构设计,最终给出选型决策框架。

前置知识#

Note

分库分表是中国互联网公司特有的工程实践——淘宝、京东、美团都在 2010 年代经历了从单库到分库分表的痛苦历程。MyCat、ShardingSphere 等中间件应运而生。与此同时,Google Spanner(2012)和 F1(2013)证明了”分布式数据库可以像单机一样用”——这催生了 TiDB(2015,PingCAP)和 CockroachDB(2015)等 NewSQL 数据库。理解这段历史,你就会明白为什么 NewSQL 被称为”分库分表的终结者”——它用分布式共识替代了中间件路由,用原生分布式事务替代了应用层补偿。

一、分库分表概述#

1.1 为什么需要分库分表#

单机数据库的扩展存在物理天花板——CPU 核数、内存容量、磁盘 IOPS、网络带宽都有上限。当业务增长到一定规模,分库分表是最直接的扩展手段。

graph TB subgraph 瓶颈["单机数据库的四大瓶颈"] B1[" 存储瓶颈<br/>单表数据量过大<br/>B+ 树层级加深"] B2[" 写入瓶颈<br/>写入 QPS 打满<br/>锁竞争加剧"] B3[" 查询瓶颈<br/>全表扫描代价高<br/>索引维护成本大"] B4[" 连接瓶颈<br/>连接数上限<br/>网络带宽饱和"] end subgraph 方案["分库分表的应对"] S1["水平拆分<br/>数据分散到多个分片"] S2["写入分摊<br/>QPS 分散到多节点"] S3["分片内扫描<br/>单分片数据量可控"] S4["连接分散<br/>多库并行服务"] end B1 --> S1 B2 --> S2 B3 --> S3 B4 --> S4 style 瓶颈 fill:#fff3e0,stroke:#e65100 style 方案 fill:#e8f5e9,stroke:#2e7d32
Note

分库分表不是唯一选择。在决定之前,先确认已穷尽单机优化手段:索引优化、查询优化、读写分离、缓存、数据归档。过早分库分表会引入不必要的复杂度。

1.2 垂直拆分#

垂直拆分按业务维度拆分数据,分为垂直分库和垂直分表。

垂直分库将不同业务模块分散到不同数据库实例:

-- 垂直分库后:按业务域拆分
-- user_db: users, user_profiles, user_settings
-- order_db: orders, order_items, refunds
-- product_db: products, categories, inventory
-- payment_db: payments, transactions, invoices

垂直分表将宽表按字段访问频率拆分:热数据(name、email)放 users_base,冷数据(avatar_url、bio、preferences)放 users_profile

维度垂直分库垂直分表
拆分依据业务域字段访问频率
目标业务解耦、独立扩展减少行大小、提升缓存效率
跨库 Join需要,通过 RPC 或中间件不需要,同一库内 Join
事务跨库事务复杂单库事务,简单

1.3 水平拆分#

水平拆分按数据行拆分,将同一张表的数据分散到多个分片,是解决单表数据量过大的核心手段。

graph LR subgraph 原始["单表 orders(1 亿行)"] T1["orders<br/>id: 1 ~ 100,000,000"] end subgraph 拆分后["水平拆分为 4 个分片"] S1["orders_0<br/>id % 4 = 0"] S2["orders_1<br/>id % 4 = 1"] S3["orders_2<br/>id % 4 = 2"] S4["orders_3<br/>id % 4 = 3"] end T1 --> S1 T1 --> S2 T1 --> S3 T1 --> S4 style 原始 fill:#ffcdd2,stroke:#c62828 style 拆分后 fill:#c8e6c9,stroke:#2e7d32
决策点选项影响
分片键用户 ID、订单 ID、时间决定数据分布均匀度
分片数量4、16、64、256过少扩展性差,过多管理复杂
分片策略范围、哈希、一致性哈希决定查询路由效率与扩容难度
分片粒度分表不分库 / 分库分表决定资源隔离程度
Warning

水平拆分一旦执行,回滚成本极高。建议在分片数量上预留扩展空间(如初始 4 分片,路由逻辑支持扩展到 64),避免频繁再平衡。

二、分片策略#

分片策略决定数据如何映射到分片,直接影响数据分布均匀性、查询效率和扩容难度。在 数据分区 中讨论了范围分区和哈希分区的原理,这里聚焦工程实践。

2.1 范围分片#

范围分片按分片键的值域划分分片,每个分片负责一段连续范围:

# 范围分片路由逻辑
RANGE_RULES = [
(1, 10_000_000, "shard_0"),
(10_000_001, 50_000_000, "shard_1"),
(50_000_001, 100_000_000, "shard_2"),
]
def route_by_range(key: int) -> str:
for start, end, shard in RANGE_RULES:
if start <= key <= end:
return shard
raise ValueError(f"Key {key} out of range")

优点:范围查询高效、扩容简单(追加新范围)。缺点:热点问题(最新数据集中在末尾分片)、数据倾斜。

2.2 哈希分片#

哈希分片对分片键计算哈希值再取模映射到分片:

// 哈希分片路由逻辑
public class HashSharding {
private final int shardCount;
public String route(String shardKey) {
int hash = murmurHash3(shardKey);
int shardIndex = Math.abs(hash) % shardCount;
return "shard_" + shardIndex;
}
private int murmurHash3(String key) {
int h = 0;
for (int i = 0; i < key.length(); i++) {
h = h * 31 + key.charAt(i);
h ^= (h >>> 16);
}
return h;
}
}

优点:数据分布均匀、热点分散。缺点:范围查询需扫描所有分片、扩容需大规模数据迁移。

2.3 一致性哈希#

一致性哈希解决哈希分片扩容时数据迁移量过大的问题,将哈希值空间组织为虚拟环:

# 一致性哈希分片实现
import hashlib
from bisect import bisect
class ConsistentHashing:
def __init__(self, replicas=150):
self.replicas = replicas
self.ring = []
self.node_map = {}
def _hash(self, key: str) -> int:
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def add_node(self, node: str):
for i in range(self.replicas):
h = self._hash(f"{node}#{i}")
self.ring.append(h)
self.node_map[h] = node
self.ring.sort()
def route(self, key: str) -> str:
h = self._hash(key)
idx = bisect(self.ring, h)
if idx == len(self.ring):
idx = 0
return self.node_map[self.ring[idx]]
graph TB subgraph 哈希环["一致性哈希环"] N1["Node A<br/>150 个虚拟节点"] N2["Node B<br/>150 个虚拟节点"] N3["Node C<br/>150 个虚拟节点"] end subgraph 扩容["扩容:添加 Node D"] E1["仅迁移 Node D 负责范围的数据<br/>迁移量 ≈ 1/4 总数据"] E2["Node A/B/C 数据不变"] end N1 --> E2 N2 --> E2 N3 --> E1 style 哈希环 fill:#e8eaf6,stroke:#283593 style 扩容 fill:#fff3e0,stroke:#e65100

2.4 查表法#

查表法维护一张分片映射表,记录每条数据所属分片,是最灵活的策略:

-- 分片映射表
CREATE TABLE shard_mapping (
entity_type VARCHAR(50) NOT NULL,
entity_id BIGINT NOT NULL,
shard_id INT NOT NULL,
PRIMARY KEY (entity_type, entity_id)
);
-- 路由查询:先查映射表,再查目标分片
SELECT shard_id FROM shard_mapping
WHERE entity_type = 'order' AND entity_id = 12345;

四种分片策略对比:

策略数据均匀性范围查询扩容迁移量灵活性复杂度
范围分片可能倾斜高效追加即可中等
哈希分片均匀全分片扫描大规模迁移
一致性哈希均匀全分片扫描最小迁移中等
查表法可控取决于映射仅改映射最高
Tip

实际项目中最常见的组合:用户 ID 用哈希分片(保证同一用户数据在同一分片),时间维度用范围分片(便于按时间查询和归档),特殊实体用查表法(灵活调整)。

三、分布式 ID 生成#

分库分表后,数据库自增 ID 不再适用——不同分片可能生成相同 ID。分布式 ID 需满足:全局唯一、趋势递增、高性能、高可用

3.1 UUID#

UUID 是最简单的方案,128 位随机生成,几乎不可能冲突:

UUID.randomUUID().toString();
// 输出: 550e8400-e29b-41d4-a716-446655440000
优点缺点
无需协调,本地生成128 位过长,存储和索引效率低
实现简单无序,B+ 树插入随机,页分裂频繁
Warning

UUID 作为 InnoDB 主键是经典性能陷阱。无序插入导致 B+ 树频繁页分裂,写入性能可能下降 30%~50%。如必须使用 UUID,考虑 UUID v7(时间戳前缀,趋势递增)。

3.2 Snowflake 雪花算法#

Snowflake 是 Twitter 开源的分布式 ID 算法,生成 64 位整数 ID,兼顾唯一性和趋势递增:

/**
* Snowflake ID 结构(64 位):
* | 1 bit | 41 bits | 10 bits | 12 bits |
* | 符号位 | 时间戳(毫秒) | 机器 ID | 序列号 |
* - 41 位时间戳:约 69 年
* - 10 位机器 ID:1024 台机器
* - 12 位序列号:每毫秒 4096 个 ID
*/
public class SnowflakeIdGenerator {
private final long epoch = 1609459200000L;
private final long sequenceMask = ~(-1L << 12L);
private final long workerIdShift = 12L;
private final long datacenterIdShift = 17L;
private final long timestampShift = 22L;
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp)
throw new RuntimeException("时钟回拨,拒绝生成 ID");
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) timestamp = tilNextMillis(lastTimestamp);
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - epoch) << timestampShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
// tilNextMillis: 自旋等待下一毫秒
}

Snowflake 的关键问题是时钟回拨——系统时钟被调回可能生成重复 ID:

策略做法优缺点
拒绝服务检测到回拨直接报错简单可靠,但影响可用性
等待追上回拨小时等待时钟追上可用性好,但延迟不可控
多 worker启用备用 worker ID需要协调分配

3.3 号段模式#

号段模式是美团 Leaf 等系统采用的方案,从中心节点批量获取 ID 段,本地消耗完后再获取下一段:

-- 号段表设计
CREATE TABLE id_segment (
biz_tag VARCHAR(64) PRIMARY KEY,
max_id BIGINT NOT NULL,
step INT NOT NULL,
version INT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 获取号段(乐观锁更新)
UPDATE id_segment
SET max_id = max_id + step, version = version + 1
WHERE biz_tag = 'order' AND version = 0;
-- 成功后,本地拥有 [1, 10000] 的 ID 段
sequenceDiagram participant App1 as 应用实例 1 participant App2 as 应用实例 2 participant DB as 号段中心 App1->>DB: 获取号段 (biz_tag=order) DB-->>App1: [1, 10000] App2->>DB: 获取号段 (biz_tag=order) DB-->>App2: [10001, 20000] Note over App1: 消耗到 10% 时<br/>异步预加载下一号段 App1->>DB: 异步预加载 DB-->>App1: [20001, 30000]

三种分布式 ID 方案对比:

方案唯一性有序性性能依赖适用场景
UUID极高无序极高非主键、追踪 ID
Snowflake趋势递增时钟主键、排序场景
号段模式严格递增数据库业务 ID、短 ID

四、跨分片查询#

分库分表后,Join、聚合、分页都可能涉及多个分片,这是最大的工程挑战。

4.1 跨分片 Join#

单库中的 Join 在分库后可能变成跨库 Join,性能急剧下降:

-- 单库:简单的用户-订单 Join
SELECT u.name, o.order_no, o.amount
FROM users u JOIN orders o ON u.id = o.user_id
WHERE u.city = '北京';
-- 分库后:应用层 Join(推荐)
-- Step 1: 查询北京用户
SELECT id, name FROM user_db.users WHERE city = '北京';
-- Step 2: 根据用户 ID 查询订单
SELECT user_id, order_no, amount
FROM order_db.orders WHERE user_id IN (1001, 1002, 1003);
Join 策略原理优点缺点
应用层 Join两次查询,内存中关联逻辑清晰网络开销大
冗余字段反范式,冗余关联字段查询简单数据一致性维护
全局表小表广播到所有分片Join 本地化更新需同步
ER 分片关联数据路由到同一分片避免跨分片灵活性差

4.2 跨分片聚合#

聚合查询需要两阶段处理——分片内聚合 + 合并:

# 跨分片聚合:两阶段处理(分片内聚合 + 合并)
def sharded_count(table, where):
results = [execute(shard, f"SELECT COUNT(*) AS c FROM {table} WHERE {where}")
for shard in shards]
return sum(r[0]['c'] for r in results) # 各分片 COUNT 求和
def sharded_avg(table, col, where):
results = [execute(shard, f"SELECT SUM({col}) AS s, COUNT({col}) AS c FROM {table} WHERE {where}")
for shard in shards]
return sum(r['s'] for r in results) / sum(r['c'] for r in results)

4.3 跨分片分页#

跨分片分页是最棘手的问题,简单的 LIMIT offset, size 在分片环境下语义不正确:

-- 错误:每个分片 LIMIT 10, 5 然后合并 → 结果不正确
-- 正确:各分片取 top N,内存归并排序
-- 分片1: SELECT * FROM orders ORDER BY created_at LIMIT 0, 15
-- 分片2: SELECT * FROM orders ORDER BY created_at LIMIT 0, 15
-- 内存归并排序后取第 10~15 条
graph TB subgraph 分页["跨分片分页流程"] Q["LIMIT 10, 5"] --> S1["分片1: LIMIT 0, 15"] Q --> S2["分片2: LIMIT 0, 15"] Q --> S3["分片3: LIMIT 0, 15"] Q --> S4["分片4: LIMIT 0, 15"] S1 --> M["内存归并排序 60 条"] S2 --> M S3 --> M S4 --> M M --> R["取第 10~15 条"] end subgraph 深分页["深度分页问题"] P["offset = 100000<br/>每分片需取 100015 条<br/>内存消耗巨大"] end R --> P style 分页 fill:#e8eaf6,stroke:#283593 style 深分页 fill:#ffcdd2,stroke:#c62828

深度分页优化方案:

方案原理优点缺点
游标分页用上一页最后一条记录的排序值作为游标性能稳定不能跳页
二次查询第一次确定范围,第二次精确查询结果精确两次查询
禁止深翻页限制最大 offset简单有效用户体验受限
搜索引擎同步到 Elasticsearch 分页性能好数据同步延迟

五、分布式事务#

分库分表后,跨分片事务是绕不开的难题。在 分布式事务 中详细讨论了 2PC、Saga、TCC 等理论,这里聚焦工程实践。

5.1 XA 事务#

XA 是最标准的分布式事务协议,基于两阶段提交:

// XA 事务示例(Java + Atomikos)
UserTransaction tx = utm.getTransaction();
tx.begin();
// 操作分片1:扣款
Connection conn1 = getShardConnection(fromUserId);
conn1.createStatement().executeUpdate(
"UPDATE accounts SET balance = balance - " + amount
+ " WHERE user_id = " + fromUserId);
// 操作分片2:加款
Connection conn2 = getShardConnection(toUserId);
conn2.createStatement().executeUpdate(
"UPDATE accounts SET balance = balance + " + amount
+ " WHERE user_id = " + toUserId);
tx.commit(); // 两阶段提交:prepare → commit/rollback

XA 的问题:协调者单点故障阻塞(prepare 后资源锁定直到 commit/rollback)、性能差

5.2 Saga 模式#

Saga 将长事务拆分为多个本地事务,每个本地事务有对应的补偿操作:

# Saga 模式:电商下单流程
class OrderSaga:
def execute(self, order):
saga = SagaBuilder() \
.step("创建订单",
action=lambda: self.order_service.create(order),
compensate=lambda: self.order_service.cancel(order.id)) \
.step("扣减库存",
action=lambda: self.inventory_service.deduct(order.sku_id, order.qty),
compensate=lambda: self.inventory_service.restore(order.sku_id, order.qty)) \
.step("扣减余额",
action=lambda: self.account_service.deduct(order.user_id, order.amount),
compensate=lambda: self.account_service.refund(order.user_id, order.amount)) \
.build()
saga.run() # 任何步骤失败,自动逆序执行补偿
sequenceDiagram participant Orch as Saga 编排器 participant Order as 订单服务 participant Inv as 库存服务 participant Acct as 账户服务 Note over Orch: 正向执行 Orch->>Order: 1. 创建订单 Orch->>Inv: 2. 扣减库存 Orch->>Acct: 3. 扣减余额 Note over Orch: 补偿执行(逆序) Orch->>Inv: 补偿:恢复库存 Orch->>Order: 补偿:取消订单

5.3 本地消息表#

本地消息表是生产环境中最常用的最终一致性方案,利用本地事务保证消息一定被写入:

-- 本地消息表
CREATE TABLE outbox_messages (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
biz_type VARCHAR(64) NOT NULL,
biz_id VARCHAR(128) NOT NULL,
payload TEXT NOT NULL,
status ENUM('PENDING','SENT','FAILED') DEFAULT 'PENDING',
retry_count INT DEFAULT 0,
max_retry INT DEFAULT 5,
INDEX idx_status_created (status, created_at)
);
-- 业务操作 + 消息写入在同一个本地事务中
BEGIN;
INSERT INTO orders (user_id, amount, status) VALUES (1001, 99.9, 'CREATED');
INSERT INTO outbox_messages (biz_type, biz_id, payload)
VALUES ('ORDER_CREATED', 'ORD_001',
'{"order_id":"ORD_001","user_id":1001,"amount":99.9}');
COMMIT;

三种分布式事务方案对比:

方案一致性性能复杂度适用场景
XA 事务强一致差(阻塞)短事务、一致性要求极高
Saga最终一致好(无锁)长流程、可补偿的业务
本地消息表最终一致好(异步)异步通知、跨服务调用
Note

在分库分表场景下,大多数业务不需要强一致的分布式事务。通过合理设计分片键(保证关联数据在同一分片)、使用本地消息表实现最终一致性、对少数强一致场景使用 XA,可覆盖 95% 以上的需求。

六、NewSQL 数据库#

NewSQL 数据库试图兼顾 NoSQL 的扩展性和关系型数据库的 ACID 事务与 SQL 接口。核心承诺:像分库分表一样水平扩展,像单机数据库一样使用 SQL

6.1 TiDB 架构#

TiDB 是 PingCAP 开发的分布式 HTAP 数据库,采用计算-存储分离的三层架构:

graph TB subgraph 计算层["TiDB Server(SQL 层)"] T1["TiDB Node 1"] T2["TiDB Node 2"] T3["TiDB Node 3"] end subgraph 协调层["PD(Placement Driver)"] PD1["PD Leader<br/>元数据/时间戳/调度"] PD2["PD Follower"] end subgraph 存储层["TiKV(行存) + TiFlash(列存)"] KV1["Region 1<br/>Raft Group"] KV2["Region 2<br/>Raft Group"] CF1["TiFlash<br/>列存副本"] end T1 --> PD1 T2 --> PD1 PD1 --> KV1 PD1 --> KV2 KV1 -.->|"异步复制"| CF1 style 计算层 fill:#e8eaf6,stroke:#283593 style 协调层 fill:#fff9c4,stroke:#f9a825 style 存储层 fill:#e0f2f1,stroke:#00695c

TiDB 的关键设计:

组件职责关键技术
TiDB ServerSQL 解析、优化、执行代价优化器、MPP 执行引擎
PD元数据、时间戳、调度Raft 选举、TSO 时间戳
TiKV行存储、事务Raft 复制、MVCC、Percolator 事务模型
TiFlash列存储、分析加速异步复制、向量化执行

TiDB 的事务模型基于 Google Percolator,采用乐观锁 + 两阶段提交:

-- TiDB 分布式事务示例
BEGIN OPTIMISTIC;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
COMMIT; -- 两阶段提交:Prewrite → Commit

6.2 CockroachDB 架构#

CockroachDB 受 Google Spanner 启发,采用无中心架构:

graph TB subgraph CRDB["CockroachDB 集群"] N1["Node 1<br/>Range 1 Leaseholder"] N2["Node 2<br/>Range 2 Leaseholder"] N3["Node 3<br/>Range 3 Leaseholder"] N4["Node 4<br/>Range 4 Leaseholder"] end N1 -.->|"HLC 混合逻辑时钟"| N2 N2 -.->|"HLC"| N3 N3 -.->|"HLC"| N4 N4 -.->|"HLC"| N1 style CRDB fill:#e8eaf6,stroke:#283593

CockroachDB 关键特性:

  • HLC(Hybrid Logical Clock):不依赖 GPS/原子钟,用混合逻辑时钟实现跨节点时间排序
  • Range 分片:按主键范围自动分片,默认 64MB 一个 Range
  • Leaseholder:每个 Range 有一个 Leaseholder 处理读写,避免所有请求都走 Raft
  • 无中心:没有 PD 这样的协调节点,元数据分散存储

6.3 OceanBase 架构#

OceanBase 是蚂蚁集团开发的分布式关系型数据库,采用 Paxos 分布式共识:

-- OceanBase:多租户资源隔离 + 兼容 MySQL 协议
CREATE RESOURCE UNIT unit_app1 MAX_CPU 4, MIN_CPU 2, MEMORY_SIZE '8G';
CREATE RESOURCE POOL pool_app1 UNIT = unit_app1, UNIT_NUM = 3;
CREATE TENANT app1_tenant RESOURCE_POOL_LIST = ('pool_app1'), CHARSET = UTF8;
-- 支持 Hash/Range/List 分区
CREATE TABLE orders (id BIGINT PRIMARY KEY, user_id BIGINT, amount DECIMAL(10,2))
PARTITION BY HASH(id) PARTITIONS 16;

三大 NewSQL 数据库架构对比:

维度TiDBCockroachDBOceanBase
架构计算存储分离(TiDB+PD+TiKV)无中心对等架构代理层+数据层+Paxos
分片Range 分片(Region)Range 分片(Range)Hash/Range/List 分区
事务模型Percolator(乐观+2PC)Parallel CommitsPaxos + 2PC
时钟TSO(PD 单点授时)HLC(混合逻辑时钟)TSOS(租户级时间戳)
兼容性MySQL 协议PostgreSQL 协议MySQL / Oracle 协议
HTAPTiFlash 列存有限行列混存

七、选型决策#

7.1 分库分表 vs NewSQL#

flowchart TB START["数据量增长,需要扩展"] --> Q1{"单机优化<br/>是否已穷尽?"} Q1 -->|否| OPT["优化索引/查询/缓存<br/>读写分离"] Q1 -->|是| Q2{"团队是否有<br/>分布式数据库运维能力?"} Q2 -->|否| SHARD["分库分表 + 中间件"] Q2 -->|是| Q3{"是否需要<br/>强一致事务?"} Q3 -->|是| Q4{"MySQL 还是<br/>PostgreSQL 生态?"} Q4 -->|MySQL| TIDB["TiDB"] Q4 -->|PostgreSQL| CRDB["CockroachDB"] Q3 -->|否| Q5{"是否需要 HTAP?"} Q5 -->|是| TIDB Q5 -->|否| SHARD style START fill:#e8eaf6,stroke:#283593 style TIDB fill:#c8e6c9,stroke:#2e7d32 style CRDB fill:#c8e6c9,stroke:#2e7d32 style SHARD fill:#fff3e0,stroke:#e65100

7.2 决策矩阵#

决策因素分库分表TiDBCockroachDBOceanBase
改造成本高(应用改造大)低(MySQL 兼容)中(PG 兼容)低(MySQL 兼容)
运维复杂度高(多实例+中间件)中(组件多但成熟)低(无中心)中(Paxos 运维)
事务一致性弱(最终一致为主)强(Percolator)强(Parallel Commits)强(Paxos+2PC)
跨分片查询差(需应用层处理)好(自动路由)好(自动路由)好(自动路由)
弹性扩展差(扩容迁移量大)好(在线扩容)好(在线扩容)好(在线扩容)
生态成熟度高(ShardingSphere)高(国内生态好)中(海外生态好)中(蚂蚁生态)

7.3 分库分表中间件选型#

中间件类型特点适用场景
ShardingSphere-JDBCClient 端轻量、无代理Java 应用、中小规模
ShardingSphere-ProxyProxy 端语言无关、独立部署多语言、运维友好
VitessProxy 端YouTube 出品、云原生Kubernetes 部署
MyCatProxy 端老牌、社区活跃遗留系统
Tip

选型核心原则:能用单机就不分库,能分库分表就不上 NewSQL,能上 NewSQL 就不造轮子。在 一致性与共识 中讨论的 CAP 定理同样适用——你不可能同时获得完美的扩展性、一致性和简单性。

八、总结#

核心要点回顾#

主题核心结论
分库分表垂直拆分解耦业务,水平拆分解决数据量;水平拆分是最后手段,不可逆
分片策略哈希分片均匀但范围查询差,范围分片反之;一致性哈希平衡扩容与均匀性
分布式 IDSnowflake 兼顾有序与性能,号段模式适合业务 ID,UUID 仅适合非主键
跨分片查询应用层 Join + 冗余字段 + 全局表组合使用;深度分页用游标替代
分布式事务优先本地消息表(最终一致),少数场景用 XA(强一致),长流程用 Saga
NewSQLTiDB(MySQL 生态+HTAP)、CockroachDB(PG 生态+无中心)、OceanBase(金融级)
选型单机优化 → 读写分离 → 分库分表 → NewSQL,逐层递进

从分库分表到 NewSQL 的演进路径#

分库分表和 NewSQL 不是对立的,而是演进关系:

  1. 阶段一:单机 MySQL/PostgreSQL,做好索引和查询优化
  2. 阶段二:读写分离,用 数据复制 缓解读压力
  3. 阶段三:分库分表,用 数据分区 的策略分散数据和写入
  4. 阶段四:引入 NewSQL,用 一致性与共识 的算法保证分布式事务

每个阶段都有其适用场景,没有银弹。理解每个方案的原理和边界,才能在正确的阶段做出正确的选择。

Note

本章讨论的分片策略与 数据分区 中的分区概念一脉相承,但视角不同:分区是数据库内核层面的数据组织方式,分库分表是应用层面的扩展手段,NewSQL 则试图将分区能力内置到数据库中,让应用无感知。理解这三者的关系,就理解了数据库扩展的完整演进路径。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

分库分表与 NewSQL:Sharding 策略与分布式数据库
https://blog.souloss.com/posts/database/sharding-and-newsql/
作者
Souloss
发布于
2024-11-02
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时