课程九:分布式数据库
课程九:分布式数据库架构演进案例
目标:数据库是许多应用的核心命脉,其扩展性和一致性至关重要。本课程将以一个典型分布式数据库系统的演进过程为例,带你深入理解数据库读写分离、分库分表、分布式事务处理、多活与弹性扩展等核心技术挑战与解决方案,深刻领悟 ACID 与 CAP 在数据库领域的工程平衡。
阶段 0:单机为王(经典的 MySQL 单实例)
系统描述
- 场景:一个典型的业务系统,需要持久化存储用户信息、订单、商品等数据。
- 功能:支持标准的 SQL 操作(INSERT/SELECT/UPDATE/DELETE),保证事务的 ACID 特性。
- 技术栈:
- 数据库:MySQL 5.7 或更高版本(使用 InnoDB 存储引擎)
- 应用连接:Java (JDBC), Python (mysqlclient) 等直接连接数据库实例。
当前架构图
graph LR
App --> MySQL("MySQL Master 单实例");
此刻的痛点:
- 读性能瓶颈:随着用户量和数据量增长,大量的读请求(通常占 80% 以上)可能将单机 CPU 和 IO 资源耗尽,导致查询变慢。
- 写性能瓶颈:在高并发写入场景下(如秒杀),锁竞争激烈,写入 QPS 受限。
- 容量限制:单机磁盘容量总有上限,TB 级别数据量下性能会显著下降。
- 单点故障:数据库服务器宕机,整个业务瘫痪。
阶段 1:读压力山大 → 读写分离先行
挑战浮现
- 应用的读 QPS 持续升高,数据库响应时间增加,用户体验下降。
- 写请求相对不多,但被大量读请求拖慢。
❓ 架构师的思考时刻:如何首先缓解最突出的读压力?
(不能直接加机器,写操作怎么办?主从复制是标准方案吗?应用层需要改动吗?)
✅ 演进方向:引入主从复制与读写分离
- 启用 MySQL 主从复制 (Master-Slave Replication):
- 配置一个主库 (Master),负责处理所有的写操作(INSERT, UPDATE, DELETE)。
- 配置一个或多个从库 (Slave),异步地从主库复制数据变更(通过 Binlog)。从库负责处理大部分读操作(SELECT)。
- 实现读写分离:
- 方案一:应用层改造。在应用代码中判断 SQL 类型,将写 SQL 发往主库,读 SQL 发往从库(可能需要负载均衡)。侵入性大。
- 方案二:引入数据库中间件/代理。使用 ProxySQL, MySQL Router, 或者 ShardingSphere-Proxy 等中间件。中间件对应用透明,自动解析 SQL 并将读写请求路由到合适的主库或从库。这是更常用的方案。
- 关注主从延迟问题:
- 由于主从复制通常是异步的,存在数据延迟。如果业务场景对读取数据的实时性要求非常高(例如,刚写入后需要立即读到),可能需要:
- 将这类读请求强制路由到主库(例如,通过 SQL Hint
/*FORCE_MASTER*/
)。 - 或者,在应用层面设计容忍短暂延迟的逻辑。
- 将这类读请求强制路由到主库(例如,通过 SQL Hint
- 由于主从复制通常是异步的,存在数据延迟。如果业务场景对读取数据的实时性要求非常高(例如,刚写入后需要立即读到),可能需要:
架构调整(引入主从与中间件):
graph TD
App --> Proxy("数据库中间件 ProxySQL / ShardingSphere-Proxy");
Proxy -- 写请求 --> Master("MySQL Master");
Proxy -- 读请求 --> Slave1("MySQL Slave 1");
Proxy -- 读请求 --> Slave2("MySQL Slave 2");
Master -- Binlog --> Slave1;
Master -- Binlog --> Slave2;
显著提升了读性能和可用性(读请求可分摊到多个从库),但写性能和单库容量瓶颈依然存在。
阶段 2:写也扛不住了 → 分库分表是终局
挑战再升级:写瓶颈与容量极限
- 随着业务持续增长,订单量、用户量爆炸式增加,即使只写主库,主库的写 QPS 也达到瓶颈。
- 单表数据量过大(如订单表达到数十亿行),索引维护成本高,查询性能(即使有索引)也急剧下降,DDL 操作(如加字段)更是成为噩梦。
- 主库的磁盘存储容量也接近极限。
❓ 架构师的思考时刻:垂直拆分已到头,数据库的终极水平扩展方案是什么?
(读写分离救不了写瓶颈和容量。必须水平拆分了。按什么维度拆?用户 ID?订单 ID?时间?怎么保证全局唯一 ID?应用层需要知道拆分的细节吗?)
✅ 演进方向:数据库水平拆分 (Sharding) + 分布式 ID
- 水平拆分策略 (Sharding Strategy):
- 选择一个合适的分片键 (Sharding Key),将数据打散到多个数据库实例(分库)或多个表中(分表)。
- 常见策略:
- 按范围 (Range):例如,按用户 ID 区间或时间范围(每月一张订单表)分片。扩展相对简单,但可能导致数据热点(如最近月份的数据访问最频繁)。
- 按哈希 (Hash):对分片键(如
user_id
或order_id
)计算哈希值,然后对分库/分表数量取模,决定数据落在哪个库/表。数据分布更均匀,但范围查询可能困难。
- 选择分片键至关重要:通常选择业务上最核心、查询时最常用的字段。需要仔细评估查询模式,避免跨分片查询(尤其是 JOIN)。常用做法是先按核心实体(如用户 ID)分库,库内再按关联实体(如订单 ID)或时间分表。
- 引入分库分表中间件:
- 使用 ShardingSphere-JDBC (集成在应用内) 或 ShardingSphere-Proxy (独立代理层),或者 MyCAT, TDDL 等。
- 中间件负责解析 SQL,根据配置的分片规则将请求路由到正确的物理库/表,并能处理结果聚合(如
ORDER BY
,GROUP BY
),尽量对应用层透明。
- 引入分布式 ID 生成器:
- 分库分表后,数据库自增主键不再全局唯一。需要引入分布式 ID 生成服务来保证主键的全局唯一性。
- 常用方案:雪花算法 (Snowflake)、数据库号段模式 (Segment)、Redis Incr、UUID(性能差,不推荐作主键)。
架构调整(数据库层深度改造):
graph TD
App --> ShardingMiddleware("分库分表中间件 ShardingSphere / MyCAT");
subgraph "分布式ID服务"
IDService("ID 生成服务 Snowflake / Segment");
end
App -- 获取全局唯一ID --> IDService;
subgraph "Sharded 数据库集群"
DBInstance1("MySQL 实例 1");
DBInstance2("MySQL 实例 2");
DBInstance3("MySQL 实例 3");
DBInstance4("MySQL 实例 4");
DBInstance1 -- 包含 Shard 0, 1, 2, 3 --> T0("Table_0..3");
DBInstance2 -- 包含 Shard 4, 5, 6, 7 --> T1("Table_4..7");
DBInstance3 -- 包含 Shard 8, 9, 10, 11 --> T2("Table_8..11");
DBInstance4 -- 包含 Shard 12, 13, 14, 15 --> T3("Table_12..15");
end
ShardingMiddleware -- SQL路由/结果聚合 --> DBInstance1;
ShardingMiddleware -- SQL路由/结果聚合 --> DBInstance2;
ShardingMiddleware -- SQL路由/结果聚合 --> DBInstance3;
ShardingMiddleware -- SQL路由/结果聚合 --> DBInstance4;
解决了写性能和容量瓶颈,但引入了分布式事务的挑战。
阶段 3:跨库操作怎么办? → 分布式事务的抉择
新挑战:保证跨分片数据一致性
- 分库分表后,一个业务操作可能需要修改分布在不同数据库实例上的数据。例如,转账操作:用户 A(在 DB 实例 1)的余额减少,用户 B(在 DB 实例 2)的余额增加。如何保证这两个操作要么都成功,要么都失败?
- 传统的本地数据库事务无法跨多个实例生效。
❓ 架构师的思考时刻:如何保证跨库操作的原子性?
(简单回滚不行了。两阶段提交 (2PC/XA)?性能太差!还有什么方案?柔性事务?最终一致性可以接受吗?)
✅ 演进方向:采用柔性事务方案 (Saga/TCC/事务消息) 或 Seata 框架
互联网场景通常无法接受 2PC/XA 的性能和锁定开销,更倾向于采用柔性事务方案,追求最终一致性:
- 可靠消息最终一致性 (基于消息队列):
- 核心思想:将跨库操作拆分成多个本地事务,通过消息队列 (MQ)(如 Kafka, RocketMQ)来驱动后续步骤。
- 例如转账:服务 A 先执行本地事务(扣款),成功后发送一条"已扣款"的消息到 MQ。服务 B 订阅该消息,消费成功后执行本地事务(加款)。
- 需要 MQ 提供可靠消息传递(至少一次送达)和消费者幂等处理能力。RocketMQ 的事务消息可以更好地保证"本地事务执行"和"消息发送"的原子性。
- TCC (Try-Confirm-Cancel):
- 对每个需要参与分布式事务的服务,实现
Try
,Confirm
,Cancel
三个接口。 Try
阶段:尝试执行业务,预留资源(如冻结金额)。Confirm
阶段:如果所有服务的Try
都成功,则调用各自的Confirm
接口,真正执行业务。Cancel
阶段:如果任何一个服务的Try
失败,则调用所有已成功Try
的服务的Cancel
接口,释放预留资源。- 对业务代码侵入性较大。
- 对每个需要参与分布式事务的服务,实现
- Saga 模式:
- 将一个长业务流程拆分成多个子事务(本地事务),每个子事务都有对应的补偿操作。
- 顺序执行子事务,如果某个子事务失败,则按相反顺序执行前面已成功子事务的补偿操作。
- 实现相对简单,但一致性保证较弱(不保证隔离性)。
- 分布式事务框架 Seata:
- 阿里巴巴开源的分布式事务解决方案,提供了 AT (自动补偿,类似 TCC 但对业务侵入小)、TCC、Saga 和 XA 多种模式。
- AT 模式是其亮点,通过代理数据源,自动生成
Try
和Cancel
逻辑(记录 SQL 执行前后的镜像,失败时自动回滚),对业务代码侵入较小。 - 引入 Seata 会增加架构的复杂度和依赖(需要部署 TC - 事务协调器)。
架构调整(以 Seata AT 模式为例):
graph TD
App --> SeataProxy("Seata 数据源代理");
SeataProxy --> ShardingMiddleware;
App -- 发起全局事务 --> TC("Seata TC 事务协调器");
subgraph "业务执行"
ShardingMiddleware -- "(分支事务1)" --> DBInstance1("涉及 Shard A");
ShardingMiddleware -- "(分支事务2)" --> DBInstance2("涉及 Shard B");
end
DBInstance1 -- 注册分支/汇报状态 --> TC;
DBInstance2 -- 注册分支/汇报状态 --> TC;
TC -- 驱动全局提交/回滚 --> App & DBInstances;
解决了跨库事务一致性问题,但对可用性和性能提出了更高要求,尤其是在线扩容。
阶段 4:业务增长不能停 → 在线 DDL 与弹性扩展
挑战再临:平滑扩容与变更
- 业务持续增长,需要增加更多的数据库分片来支撑更大的数据量和并发。传统的停机维护窗口越来越难以接受。
- 如何在不中断服务的情况下,完成数据库的扩容(增加分片数量)和结构变更(如加表、加字段 DDL 操作)?
❓ 架构师的思考时刻:如何让数据库集群像应用一样具备弹性?
(停机扩容肯定不行。有没有在线迁移数据的方案?DDL 变更怎么平滑进行?)
✅ 演进方向:探索 NewSQL 或采用成熟的在线 DDL/迁移工具
- NewSQL 数据库:
- 一些新兴的分布式数据库(常被称为 NewSQL),如 TiDB, CockroachDB, Google Spanner 等,天生就具备在线弹性扩展能力。
- 它们通常采用类似 RocksDB 的底层存储引擎和 Raft/Paxos 一致性协议,可以自动进行数据的 rebalancing 和 region splitting/merging,支持在线加减节点。
- 并且大多兼容 MySQL 协议,迁移成本相对可控。
- 在线 DDL 工具 (针对 MySQL Sharding):
- 对于继续使用 MySQL 分片集群的场景,可以使用 gh-ost 或 pt-online-schema-change 等工具来实现在线的、对业务影响较小的 DDL 操作。
- 这些工具通过创建"影子表"、数据拷贝和触发器同步增量变更的方式工作。
- 在线数据迁移与扩容方案 (针对 MySQL Sharding):
- 数据库扩容(如从 16 分片扩展到 32 分片)通常需要复杂的数据迁移过程。
- 常用策略包括:
- 双写:在迁移期间,应用同时写入新旧两个分片集群。
- 数据同步:使用 Canal 等工具订阅旧集群的 Binlog,将增量变更实时同步到新集群。
- 全量校验与切换:完成全量数据迁移和增量同步后,进行数据校验,最终将流量切换到新集群。
- 一些数据库中间件(如 ShardingSphere 的 ElasticJob)或云厂商服务提供了辅助数据迁移的功能。
- (可选) 拥抱云数据库:
- 云厂商提供的分布式数据库服务(如 AWS Aurora, 阿里云 PolarDB-X)通常内置了弹性伸缩和在线变更的能力,可以大大简化运维复杂度,但会锁定供应商。
架构调整(以采用 TiDB 为例):
graph TD
App --> LoadBalancer("负载均衡器 如 Nginx/HAProxy");
LoadBalancer --> TiDBServer1("TiDB Server 无状态SQL层 实例1");
LoadBalancer --> TiDBServer2("TiDB Server 无状态SQL层 实例2");
LoadBalancer --> TiDBServerN("TiDB Server 无状态SQL层 实例N");
TiDBServer1 --> PDCluster("PD Cluster - Placement Driver 元数据/调度");
TiDBServer2 --> PDCluster;
TiDBServerN --> PDCluster;
PDCluster -- Raft --> PDC1("PD 实例1");
PDCluster -- Raft --> PDC2("PD 实例2");
PDCluster -- Raft --> PDC3("PD 实例3");
subgraph "TiKV Cluster (分布式 KV 存储层)"
TiKVServer1("TiKV Server 实例1") -- "存储 Region 1, 4, 7 ..." --> Store1("Store1");
TiKVServer2("TiKV Server 实例2") -- "存储 Region 2, 5, 8 ..." --> Store2("Store2");
TiKVServer3("TiKV Server 实例3") -- "存储 Region 3, 6, 9 ..." --> Store3("Store3");
TiKVServer1 <-- Raft --> TiKVServer2;
TiKVServer2 <-- Raft --> TiKVServer3;
TiKVServer1 <-- Raft --> TiKVServer3;
end
TiDBServer1 -- 读写 --> TiKVCluster;
TiDBServer2 -- 读写 --> TiKVCluster;
TiDBServerN -- 读写 --> TiKVCluster;
PDCluster -- Region调度/心跳 --> TiKVCluster;
TiDB 等 NewSQL 数据库提供了更原生的分布式能力和弹性。
阶段 5:单一模型不够用 → 探索 HTAP 与多模数据库
新挑战:混合负载的诉求
- 业务系统不仅需要处理高并发的在线事务处理 (OLTP),还需要进行复杂的在线分析处理 (OLAP),例如实时报表、用户行为分析、BI 查询等。
- 传统的做法是将 OLTP 数据通过 ETL (Extract, Transform, Load) 导入到数据仓库 (如 Hive, ClickHouse) 进行分析,但存在数据延迟和架构复杂的问题。
- 能否在同一个数据库系统中同时高效地支持 OLTP 和 OLAP 负载?这就是 HTAP (Hybrid Transactional/Analytical Processing) 的目标。
❓ 架构师的思考时刻:如何在不影响在线交易的前提下,进行实时数据分析?
(ETL 太慢了。能不能直接在生产库上跑分析?怎么隔离资源?列式存储对分析更有利?)
✅ 演进方向:采用 HTAP 数据库 或 构建实时数据链路
- 采用原生 HTAP 数据库:
- 一些分布式数据库(如 TiDB)通过引入列式存储引擎(如 TiFlash)作为特殊副本,实现了 HTAP 架构。
- OLTP 请求路由到基于行式存储 (TiKV) 的副本,保证事务性能。
- OLAP 请求路由到基于列式存储 (TiFlash) 的副本,利用列存的优势(高压缩率、按列扫描)加速分析查询。
- 数据通过 Raft 协议自动从行存同步到列存,保证实时性。
- 构建实时数据同步链路 (CDC + OLAP 数据库):
- 如果继续使用 MySQL 或其他 OLTP 数据库,可以通过 CDC (Change Data Capture) 工具(如 Canal, Debezium)实时捕获数据库的变更日志 (Binlog)。
- 将变更数据实时同步到专门的 OLAP 数据库(如 ClickHouse, Doris, StarRocks)中。
- OLAP 查询直接在分析型数据库上执行,与 OLTP 负载分离。
- 这种方案灵活性更高,可以选择最优的 OLAP 引擎,但需要维护数据同步链路。
- 资源隔离:
- 无论是哪种方案,都需要确保 OLAP 查询不会影响 OLTP 的性能。可以通过资源组、优先级队列、物理隔离等方式进行资源隔离。
架构调整(以 TiDB HTAP 为例):
graph TD
subgraph "应用层"
OLTP_App("在线交易应用") --> TiDB_SQL("TiDB SQL 层");
OLAP_App("数据分析/报表应用") --> TiDB_SQL;
end
subgraph "TiDB 集群"
TiDB_SQL -- "(OLTP 请求)" --> TiKV_Cluster("TiKV Cluster 行式存储 Raft 副本");
TiDB_SQL -- "(OLAP 请求)" --> TiFlash_Cluster("TiFlash Cluster 列式存储 Raft 副本");
TiKV_Cluster -- "Raft Learner 同步" --> TiFlash_Cluster;
TiDB_SQL --> PD_Cluster("PD Cluster 调度/元数据");
PD_Cluster --> TiKV_Cluster;
PD_Cluster --> TiFlash_Cluster;
end
实现了在单一系统内高效处理混合负载的需求。
总结:分布式数据库的演进之路
阶段 | 核心挑战 | 关键解决方案 | 代表技术/模式 |
---|---|---|---|
0. 单机 | 性能/容量/单点 | (无法解决) | MySQL 单实例 |
1. 读写分离 | 读性能瓶颈 | 主从复制 + 读写分离中间件 | MySQL Replication, ProxySQL/ShardingSphere-Proxy |
2. 分库分表 | 写瓶颈/容量极限 | 水平拆分 (Sharding) + 分布式 ID | ShardingSphere/MyCAT, Snowflake/Segment |
3. 分布式事务 | 跨分片数据一致性 | 柔性事务 (Saga/TCC/事务消息) / Seata 框架 | Kafka/RocketMQ, Seata (AT/TCC/Saga) |
4. 弹性扩展 | 在线 DDL/扩容困难 | NewSQL / 在线 DDL 与迁移工具 / 云数据库 | TiDB/CockroachDB, gh-ost/pt-osc, Canal, PolarDB-X |
5. HTAP/多模 | OLTP 与 OLAP 混合负载 | 原生 HTAP 数据库 / CDC + OLAP 数据库 | TiDB+TiFlash, Debezium + ClickHouse/Doris |
课程设计亮点与思考
- 直击核心痛点:课程围绕数据库扩展性、一致性、可用性这三大核心挑战展开,层层递进。
- 技术选型与权衡:清晰展示了在不同阶段面临的技术选型(如读写分离方案、分片策略、分布式事务方案、扩展方案),并隐含了其背后的优劣权衡。
- 覆盖关键概念:系统性地讲解了主从复制、分库分表、分布式 ID、分布式事务(2PC、柔性事务)、NewSQL、HTAP 等数据库领域的核心概念与技术。
- 工业界方案参照:结合了业界广泛使用的开源中间件(ShardingSphere, MyCAT, ProxySQL)、框架(Seata)、数据库(TiDB, ClickHouse)和工具(Canal, gh-ost),具有很强的实践指导意义。
- 面试热点密集:课程内容覆盖了数据库和分布式系统面试中的绝大多数高频考点。
掌握分布式数据库的架构演进,是构建可靠、可扩展的现代应用服务的必备内功。理解不同方案的原理和权衡,才能在实际工作中做出合理的架构决策。