跳转至

课程九:分布式数据库

课程九:分布式数据库架构演进案例

目标:数据库是许多应用的核心命脉,其扩展性和一致性至关重要。本课程将以一个典型分布式数据库系统的演进过程为例,带你深入理解数据库读写分离、分库分表、分布式事务处理、多活与弹性扩展等核心技术挑战与解决方案,深刻领悟 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 持续升高,数据库响应时间增加,用户体验下降。
  • 写请求相对不多,但被大量读请求拖慢。

❓ 架构师的思考时刻:如何首先缓解最突出的读压力?

(不能直接加机器,写操作怎么办?主从复制是标准方案吗?应用层需要改动吗?)

✅ 演进方向:引入主从复制与读写分离

  1. 启用 MySQL 主从复制 (Master-Slave Replication)
    • 配置一个主库 (Master),负责处理所有的写操作(INSERT, UPDATE, DELETE)。
    • 配置一个或多个从库 (Slave),异步地从主库复制数据变更(通过 Binlog)。从库负责处理大部分读操作(SELECT)。
  2. 实现读写分离
    • 方案一:应用层改造。在应用代码中判断 SQL 类型,将写 SQL 发往主库,读 SQL 发往从库(可能需要负载均衡)。侵入性大。
    • 方案二:引入数据库中间件/代理。使用 ProxySQL, MySQL Router, 或者 ShardingSphere-Proxy 等中间件。中间件对应用透明,自动解析 SQL 并将读写请求路由到合适的主库或从库。这是更常用的方案。
  3. 关注主从延迟问题
    • 由于主从复制通常是异步的,存在数据延迟。如果业务场景对读取数据的实时性要求非常高(例如,刚写入后需要立即读到),可能需要:
      • 将这类读请求强制路由到主库(例如,通过 SQL Hint /*FORCE_MASTER*/)。
      • 或者,在应用层面设计容忍短暂延迟的逻辑。

架构调整(引入主从与中间件)

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

  1. 水平拆分策略 (Sharding Strategy)
    • 选择一个合适的分片键 (Sharding Key),将数据打散到多个数据库实例(分库)或多个表中(分表)。
    • 常见策略
      • 按范围 (Range):例如,按用户 ID 区间或时间范围(每月一张订单表)分片。扩展相对简单,但可能导致数据热点(如最近月份的数据访问最频繁)。
      • 按哈希 (Hash):对分片键(如 user_idorder_id)计算哈希值,然后对分库/分表数量取模,决定数据落在哪个库/表。数据分布更均匀,但范围查询可能困难。
    • 选择分片键至关重要:通常选择业务上最核心、查询时最常用的字段。需要仔细评估查询模式,避免跨分片查询(尤其是 JOIN)。常用做法是先按核心实体(如用户 ID)分库,库内再按关联实体(如订单 ID)或时间分表。
  2. 引入分库分表中间件
    • 使用 ShardingSphere-JDBC (集成在应用内) 或 ShardingSphere-Proxy (独立代理层),或者 MyCAT, TDDL 等。
    • 中间件负责解析 SQL,根据配置的分片规则将请求路由到正确的物理库/表,并能处理结果聚合(如 ORDER BY, GROUP BY),尽量对应用层透明。
  3. 引入分布式 ID 生成器
    • 分库分表后,数据库自增主键不再全局唯一。需要引入分布式 ID 生成服务来保证主键的全局唯一性。
    • 常用方案:雪花算法 (Snowflake)数据库号段模式 (Segment)Redis IncrUUID(性能差,不推荐作主键)。

架构调整(数据库层深度改造)

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 的性能和锁定开销,更倾向于采用柔性事务方案,追求最终一致性

  1. 可靠消息最终一致性 (基于消息队列)
    • 核心思想:将跨库操作拆分成多个本地事务,通过消息队列 (MQ)(如 Kafka, RocketMQ)来驱动后续步骤。
    • 例如转账:服务 A 先执行本地事务(扣款),成功后发送一条"已扣款"的消息到 MQ。服务 B 订阅该消息,消费成功后执行本地事务(加款)。
    • 需要 MQ 提供可靠消息传递(至少一次送达)和消费者幂等处理能力。RocketMQ 的事务消息可以更好地保证"本地事务执行"和"消息发送"的原子性。
  2. TCC (Try-Confirm-Cancel)
    • 对每个需要参与分布式事务的服务,实现 Try, Confirm, Cancel 三个接口。
    • Try 阶段:尝试执行业务,预留资源(如冻结金额)。
    • Confirm 阶段:如果所有服务的 Try 都成功,则调用各自的 Confirm 接口,真正执行业务。
    • Cancel 阶段:如果任何一个服务的 Try 失败,则调用所有已成功 Try 的服务的 Cancel 接口,释放预留资源。
    • 对业务代码侵入性较大。
  3. Saga 模式
    • 将一个长业务流程拆分成多个子事务(本地事务),每个子事务都有对应的补偿操作。
    • 顺序执行子事务,如果某个子事务失败,则按相反顺序执行前面已成功子事务的补偿操作
    • 实现相对简单,但一致性保证较弱(不保证隔离性)。
  4. 分布式事务框架 Seata
    • 阿里巴巴开源的分布式事务解决方案,提供了 AT (自动补偿,类似 TCC 但对业务侵入小)TCCSagaXA 多种模式。
    • AT 模式是其亮点,通过代理数据源,自动生成 TryCancel 逻辑(记录 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/迁移工具

  1. NewSQL 数据库
    • 一些新兴的分布式数据库(常被称为 NewSQL),如 TiDB, CockroachDB, Google Spanner 等,天生就具备在线弹性扩展能力。
    • 它们通常采用类似 RocksDB 的底层存储引擎和 Raft/Paxos 一致性协议,可以自动进行数据的 rebalancing 和 region splitting/merging,支持在线加减节点。
    • 并且大多兼容 MySQL 协议,迁移成本相对可控。
  2. 在线 DDL 工具 (针对 MySQL Sharding)
    • 对于继续使用 MySQL 分片集群的场景,可以使用 gh-ostpt-online-schema-change 等工具来实现在线的、对业务影响较小的 DDL 操作。
    • 这些工具通过创建"影子表"、数据拷贝和触发器同步增量变更的方式工作。
  3. 在线数据迁移与扩容方案 (针对 MySQL Sharding)
    • 数据库扩容(如从 16 分片扩展到 32 分片)通常需要复杂的数据迁移过程。
    • 常用策略包括:
      • 双写:在迁移期间,应用同时写入新旧两个分片集群。
      • 数据同步:使用 Canal 等工具订阅旧集群的 Binlog,将增量变更实时同步到新集群。
      • 全量校验与切换:完成全量数据迁移和增量同步后,进行数据校验,最终将流量切换到新集群。
    • 一些数据库中间件(如 ShardingSphere 的 ElasticJob)或云厂商服务提供了辅助数据迁移的功能。
  4. (可选) 拥抱云数据库
    • 云厂商提供的分布式数据库服务(如 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 数据库 或 构建实时数据链路

  1. 采用原生 HTAP 数据库
    • 一些分布式数据库(如 TiDB)通过引入列式存储引擎(如 TiFlash)作为特殊副本,实现了 HTAP 架构。
    • OLTP 请求路由到基于行式存储 (TiKV) 的副本,保证事务性能。
    • OLAP 请求路由到基于列式存储 (TiFlash) 的副本,利用列存的优势(高压缩率、按列扫描)加速分析查询。
    • 数据通过 Raft 协议自动从行存同步到列存,保证实时性。
  2. 构建实时数据同步链路 (CDC + OLAP 数据库)
    • 如果继续使用 MySQL 或其他 OLTP 数据库,可以通过 CDC (Change Data Capture) 工具(如 Canal, Debezium)实时捕获数据库的变更日志 (Binlog)。
    • 将变更数据实时同步到专门的 OLAP 数据库(如 ClickHouse, Doris, StarRocks)中。
    • OLAP 查询直接在分析型数据库上执行,与 OLTP 负载分离。
    • 这种方案灵活性更高,可以选择最优的 OLAP 引擎,但需要维护数据同步链路。
  3. 资源隔离
    • 无论是哪种方案,都需要确保 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

课程设计亮点与思考

  1. 直击核心痛点:课程围绕数据库扩展性、一致性、可用性这三大核心挑战展开,层层递进。
  2. 技术选型与权衡:清晰展示了在不同阶段面临的技术选型(如读写分离方案、分片策略、分布式事务方案、扩展方案),并隐含了其背后的优劣权衡。
  3. 覆盖关键概念:系统性地讲解了主从复制、分库分表、分布式 ID、分布式事务(2PC、柔性事务)、NewSQL、HTAP 等数据库领域的核心概念与技术。
  4. 工业界方案参照:结合了业界广泛使用的开源中间件(ShardingSphere, MyCAT, ProxySQL)、框架(Seata)、数据库(TiDB, ClickHouse)和工具(Canal, gh-ost),具有很强的实践指导意义。
  5. 面试热点密集:课程内容覆盖了数据库和分布式系统面试中的绝大多数高频考点。

掌握分布式数据库的架构演进,是构建可靠、可扩展的现代应用服务的必备内功。理解不同方案的原理和权衡,才能在实际工作中做出合理的架构决策。