首页 / 资讯中心 / 文章详情

一台 MySQL 不够用了怎么办?从主从复制、读写分离到分库分表

一台 MySQL 不够用了怎么办?从主从复制、读写分离到分库分表 ★ FEATURED ARTICLE
前两篇我们学习了 MySQL 的事务、MVCC、锁和索引。通过这些知识已经能够理解一台 MySQL 如何完成数据存储、查询和并发控制。但随着微服务系统不断发展单台 MySQL 可能逐渐成为整个系统的瓶颈。例如一个电商系统最初只有几十万条订单一台 MySQL 就能处理全部请求。几年以后订单数据增长到几亿条同时每天有大量用户查询历史订单后台服务还需要不断写入新的交易记录。这时候数据库可能出现查询越来越慢。数据库 CPU 和 I/O 压力持续上升。单台机器存储容量逐渐不足。某台数据库服务器故障影响整个系统。大量读请求和写请求互相争夺数据库资源。面对这些问题常见的解决方案包括主从复制、读写分离和分库分表。但它们解决的问题并不相同。主从复制主要提供数据副本读写分离利用副本分担读请求而分库分表则可以把数据和访问压力分散到多个存储单元。理解它们的区别比直接记住几个架构名词更重要。一、MySQL 出现瓶颈就一定要分库分表吗假设我们的订单系统采用最初的架构Order Service | v MySQL | orders 表随着业务增长查询开始变慢。一种很直接的想法是一台 MySQL 不够用了那就拆成多台 MySQL。但实际设计时不应该一开始就选择分库分表。因为 MySQL 的性能问题可能有不同原因。1. SQL 和索引设计不合理例如SELECT*FROMordersWHEREuser_id888ORDERBYcreated_atDESCLIMIT20;如果orders表数据量很大同时缺少合适的索引就可能需要扫描大量记录并进行额外排序。这时首先应该分析 SQL 执行计划。例如根据真实查询负载考虑CREATEINDEXidx_user_timeONorders(user_id,created_at);如果通过合理的索引就能显著减少查询开销那么没有必要立即引入分库分表。2. 读请求太多假设数据库每秒需要处理订单查询8000 次 订单创建500 次主要压力来自查询。这时可以考虑通过缓存减少重复读取或者通过 MySQL 副本分担读请求。3. 写请求太多如果系统每秒需要处理大量订单写入INSERT orders UPDATE orders UPDATE inventory而单台 MySQL 的写入能力已经成为瓶颈那么增加只读副本通常不能直接解决这个问题。因为在常见的单主复制架构中写操作仍然集中在主库。4. 数据容量太大假设订单表增长到数亿条数据库实例的存储容量、维护窗口和大表管理成本都越来越高。这时就可能需要按照业务或数据范围拆分存储。因此数据库扩展前应该先判断问题属于哪一种问题优先考虑的方向SQL 查询效率低索引、SQL、执行计划大量重复读取Redis 缓存读请求压力过大主从复制、读写分离单点故障风险副本、故障切换、备份单主库写入能力不足数据拆分、业务写入优化单实例数据容量不足归档、分区或分库分表不是所有 MySQL 性能问题都需要通过增加数据库服务器来解决。二、主从复制为什么一份数据库要保存多个副本假设系统最初只有一台数据库Order Service | v MySQL A如果 MySQL A 突然故障订单服务就可能无法正常读取和写入数据库。如果我们能在另一台服务器上保存相应的数据副本就可以为故障恢复提供基础。这就是 MySQL Replication复制的一个重要作用。一种常见架构是MySQL Primary | ---------------- | | v v Replica 1 Replica 2其中**Primary主库**负责处理主要写入。**Replica从库或副本**通过复制机制获取主库产生的数据变化。以前经常使用 Master / Slave 这样的名称现在更常使用 Primary / Replica。1. 主从复制是不是每次都复制整张表不是。假设主库已经有orders 表 10001 | 888 | UNPAID 10002 | 889 | PAID现在 Order Service 执行UPDATEordersSETstatusPAIDWHEREid10001;主从复制不需要每次把整个orders表重新发送给从库。MySQL 可以利用 Binary LogBinlog二进制日志记录数据库的数据变更事件。副本根据复制机制获取这些事件并在本地应用相应的数据变化。2. Binlog 和 Relay Log 分别负责什么可以通过下面的流程理解MySQL Primary | | 执行事务 v Binary Log | | 复制传输 v Replica I/O | v Relay Log | v Replica Applier | v Replica 数据Binlog记录主库产生的相关数据库变更事件是 MySQL 复制的重要数据来源。Relay Log副本接收来自主库的复制事件后将它们保存到本地中继日志。Applier副本上的应用线程负责执行接收到的复制事件使副本数据逐步跟上主库。需要注意Binlog 可以有不同的记录格式。实际工程中Row-Based Replication基于行的复制十分常见。我们可以把复制过程简化理解为主库提交数据修改 | v 相关变化记录到 Binlog | v 从库接收复制事件 | v 保存到 Relay Log | v 应用事件更新本地数据MySQL 事务提交与 Binlog、InnoDB 日志之间还涉及内部的一致性协调机制但本篇重点先放在主从复制的基本流程上。3. 主从复制是否等于强一致不等于。尤其是在常见的异步复制模式下主库提交成功并不需要等待所有副本都完成相同的数据修改。所以可能发生Primary 订单已经更新为 PAID Replica 订单仍然是 UNPAID这并不一定代表复制已经故障。也可能只是副本还没有应用最新的变更事件。这种现象就是我们接下来要讨论的复制延迟。三、读写分离为什么增加从库可以减轻读压力有了主从复制之后我们就拥有了多份数据库数据。最初所有请求都访问主库Order Service | v Primary现在可以采用Order Service | 读写路由 / \ / \ 写请求 读请求 | | v ---------- Primary | | v v Replica 1 Replica 2这就是常见的读写分离。1. 写操作主要进入主库例如INSERTINTOorders(id,user_id,status)VALUES(10001,888,UNPAID);以及UPDATEordersSETstatusPAIDWHEREid10001;在典型的单主架构中这些写操作由 Primary 负责。随后主库通过复制机制将变更传递给副本。2. 部分读请求分配给从库例如SELECT*FROMordersWHEREuser_id888;或者SELECT*FROMordersWHEREstatusPAID;如果业务允许读取略有延迟的数据就可以将部分查询请求分配给 Replica。假设原来 Primary 每秒处理 10000 次查询现在Primary 每秒处理 2000 次查询 Replica 1 每秒处理 4000 次查询 Replica 2 每秒处理 4000 次查询这只是用于说明流量分配的示例不代表数据库请求一定能够平均分配也不代表增加副本后吞吐量就会线性提升。实际性能取决于 SQL、机器资源、缓存命中、复制应用负载和查询路由等因素。但核心思想很清楚将部分读请求从主库转移到副本使主库保留更多资源处理写入。3. 谁来决定请求访问主库还是从库通常有两种实现方向。一种是在应用层路由。例如 Order Service 的数据访问层根据业务要求选择不同的数据源。创建订单 | v Primary 查询普通历史订单列表 | v Replica另一种是通过数据库代理提供读写路由能力。例如Order Service | v Database Proxy | ---- Primary | ---- Replica 1 | ---- Replica 2应用访问代理代理根据相应规则把请求转发到目标数据库。但无论采用哪一种方式都不能简单认为SELECT 一律去 Replica INSERT / UPDATE 一律去 Primary因为某些 SELECT 对数据新鲜度有严格要求。例如用户刚完成支付马上查询订单状态。这就涉及主从复制中最重要的一致性问题。四、主从延迟为什么刚刚更新成功却查到了旧数据假设用户支付完成后Order Service 修改订单状态UPDATEordersSETstatusPAIDWHEREid10001;该事务已经在 Primary 成功提交。主库中order_id 10001 status PAID但是 Replica 还没有应用这条复制事件。副本中暂时仍然是order_id 10001 status UNPAID此时用户马上查询订单GET /orders/10001如果请求被路由到 Replica就可能返回{order_id:10001,status:UNPAID}用户明明已经支付成功却看到订单仍然未支付。这就是复制延迟导致的读取旧数据问题。1. 为什么主库和从库之间会有延迟因为主库提交事务、复制传输和副本应用事件不是完全同步完成的一个操作。可以分成三个阶段T1Primary 提交事务 | v T2Replica 接收到 Binlog 事件 | v T3Replica 应用事务完成在 T1 与 T3 之间副本可能还没有最新的数据状态。造成延迟的原因包括主库写入量很大副本需要处理大量复制事件。副本磁盘或 CPU 资源不足。某些复制事务执行时间较长。网络传输出现延迟。副本同时承担大量查询影响复制应用性能。因此主从复制保证的是数据变更能够按照相应复制机制传播而不是每一次读取都天然看到主库刚提交的最新结果。2. 如何保证用户写完以后能读到自己的结果我们之前学习过的一个直接方案是关键写操作完成后将需要立即看到最新结果的读取路由到主库。例如用户完成支付 | v Primary 更新订单 | v 用户立即查询订单 | v 仍然读取 Primary这能够避免因为副本尚未完成复制而从副本读到旧状态。不过如果大量读请求都必须走主库就会降低读写分离带来的收益。所以另一种思路是只对需要读后写一致性的请求使用主库普通列表、历史记录等允许短暂延迟的查询仍然可以走副本。3. GTID 可以解决什么问题GTIDGlobal Transaction Identifier全局事务标识可以用于标识 MySQL 复制中的事务。例如可以理解成事务 T100 已经在 Primary 提交如果应用需要从 Replica 读取最新的业务结果可以通过 GTID 相关机制判断或等待副本应用到指定的事务位置。例如Primary 提交事务 T100 | v 获取相应事务标识 | v 等待 Replica 应用 T100 | v 从 Replica 查询这种机制能够帮助实现具有明确进度要求的副本读取。但必须注意GTID 只是提供事务身份和复制进度协调基础不是开启 GTID 后所有从库读取就自动变成强一致。应用仍然需要选择目标副本、等待相应事务被应用并处理等待超时的情况。如果等待超时可以根据业务要求回退读取主库或者明确返回当前无法满足新鲜度要求。4. 半同步复制是不是就没有延迟了不是。MySQL 的 Semi-Synchronous Replication半同步复制主要改变主库等待副本确认的机制。简化理解Primary | | 发送复制事件 v Replica | | 确认已接收并记录相关事件 v Primary 获得确认但是副本确认收到复制事件不等于副本已经执行并应用该事务。所以即使使用半同步复制副本仍然可能存在读取延迟。半同步复制可以在满足其确认条件时降低部分故障丢失窗口但它并不自动提供所有副本读取的强一致性也不意味着绝对不会丢数据。五、主从复制能解决数据库高可用吗增加 Replica 后系统拥有多份数据。如果 Primary 出现故障可以考虑将符合条件的 Replica 提升为新的 Primary。例如故障前 Primary A | ---- Replica B | ---- Replica CPrimary A 故障Primary A × | v 选择合适的 Replica | v Replica B 成为新 Primary这样系统可以恢复写入服务。但这里还有两个需要特别注意的问题。1. 主从复制不等于自动故障切换如果只是搭建了一个 Primary 和两个 Replica并不代表 Primary 故障后系统就一定会自动完成切换。还需要有相应的故障检测、主库选举或提升、访问路由更新等机制。尤其要避免旧 Primary 恢复后继续接受写入形成多个独立写入源。否则可能出现数据分叉。2. 异步复制可能丢失已提交但尚未复制的数据假设T1Primary 提交订单 10001 T2应用收到提交成功响应 T3Primary 发生故障 T4Replica 尚未接收到该事务如果此时将落后的 Replica 提升为新 Primary那么新主库可能没有订单 10001。从用户角度看订单提交成功 | v 数据库发生故障 | v 切换后的数据库没有这条订单这就是异步复制在故障切换时存在的数据丢失窗口。所以主从复制能够提供高可用的基础但不能单独保证自动切换、零数据丢失和业务强一致。另外复制也不能替代备份。例如某个错误的 SQL 把大量订单删除DELETEFROMorders;如果这个操作被提交并复制到 Replica那么副本也可能执行相应删除。因此高可用、副本复制和数据备份解决的是不同问题。六、为什么主从复制不能直接解决写入瓶颈这是区分读写分离与分库分表的关键。假设原来一台 MySQLPrimary 每秒 5000 次读取 1000 次写入增加两台 Replica 后Primary 1000 次写入 部分读取 Replica 1 分担读取 Replica 2 分担读取虽然读请求可以分散但所有写入仍然主要集中在 Primary。例如INSERTINTOorders(...);UPDATEinventorySETstockstock-1WHEREproduct_id1001;这些操作在单主架构下仍然需要由 Primary 处理。而且副本也需要应用主库产生的写入变更。因此增加 Replica 并不是把一条写操作平均分配到三台机器执行。如果真正的瓶颈来自大量写入或者数据量已经超过单个数据库实例适合管理的范围就需要考虑进一步拆分。这就是分库分表的主要应用场景之一。七、分库分表到底是在拆什么“分库分表”经常被当成一个整体概念但实际上包含不同的拆分方向。我们可以先区分垂直拆分 水平拆分1. 垂直拆分按照业务职责拆分假设最初整个电商系统只有一个数据库ecommerce_db | -- users | -- orders | -- order_items | -- products | -- inventory | -- payments随着系统发展我们可以按照业务能力划分数据库所有权。例如User Service | v user_db | -- usersOrder Service | v order_db | -- orders -- order_itemsInventory Service | v inventory_db | -- inventory这种方式属于按照业务领域进行垂直拆分的典型形式。它和第一篇讨论的服务拆分有直接联系。核心目标是让不同业务能力拥有清晰的数据边界并能够按照自己的需求独立维护。例如Order Service应负责订单数据的写入与修改。而Payment Service不应该随意绕过订单服务直接修改orders表中的内部字段。需要注意垂直拆分不一定意味着每个业务服务都占有独立的物理数据库服务器。逻辑数据库、数据库实例和物理服务器是不同层次的概念。2. 水平拆分把同一类数据分散到多个存储单元假设orders表已经有数亿条记录。即使整个 Order Service 只管理订单业务单个数据库实例也可能面临容量和写入压力。这时可以将同一种业务数据按某个规则划分到多个分片。例如Order Service | v Sharding Router | -- order_db_0 | -- order_db_1 | -- order_db_2 | -- order_db_3四个数据库都用于保存订单数据但每个数据库只承担其中一部分订单。这就是水平分库的思路。也可以在单个数据库中创建多张订单表orders_0 orders_1 orders_2 orders_3由应用根据分片规则选择目标表。不过把一张表拆成多张仍位于同一数据库实例中的表不代表已经扩展了该实例的物理 CPU、I/O 或存储能力。如果希望真正分散单实例的资源瓶颈通常还需要让数据分布到不同的数据库实例或存储资源上。垂直拆分和水平拆分怎么区分可以用一句话记忆垂直拆分主要回答“哪些业务数据应该分开管理”。水平拆分主要回答“同一类业务数据太多时如何分散存储和访问压力”。例如垂直拆分 用户数据 | 订单数据 | 支付数据水平拆分 订单数据 1 | 订单数据 2 | 订单数据 3实际系统也可以结合两种方式先按照业务能力划分数据边界再对增长特别快的业务表进行水平拆分。八、水平分库之后Order Service 怎么知道数据在哪个库假设订单数据分布在四个数据库中order_db_0 order_db_1 order_db_2 order_db_3用户 888 查询自己的订单SELECT*FROMordersWHEREuser_id888;这时候 Order Service 必须知道用户 888 的订单到底应该去哪个数据库查询这就是 Sharding Key分片键和分片路由需要解决的问题。1. 根据 user_id 进行分片假设我们决定使用user_id作为分片键。一个简单的规则是shard_id user_id % 4例如user_id计算过程目标数据库888888 % 4 0order_db_0889889 % 4 1order_db_1890890 % 4 2order_db_2891891 % 4 3order_db_3那么用户 888 创建订单时Order Service | | user_id 888 v Sharding Router | | 888 % 4 0 v order_db_0查询用户 888 的订单时也执行相同的路由计算。这样就不需要向四个数据库分别查询。2. 为什么分片键很重要假设我们使用user_id分片。那么SELECT*FROMordersWHEREuser_id888;很容易定位到order_db_0但如果查询条件只有SELECT*FROMordersWHEREid10001;问题就出现了。因为仅凭订单 ID我们不知道它最初属于哪个用户也就无法直接使用user_id % 4计算目标分片。这时候可能需要根据订单 ID 查询一张额外的路由映射表。通过订单 ID 的生成规则推导分片位置。根据业务上下文获取对应user_id。或者向多个分片发起查询。这也是为什么分片键不能随意选择。分片键决定了数据如何分布也决定了以后哪些查询能够被直接路由到单个分片。3. 为什么不能只考虑数据分布均不均匀例如我们选择user_id % 4如果大部分用户的订单量差不多那么数据可能比较均匀。但如果某个用户每天产生几百万条订单而普通用户只产生几条订单那么这个大客户可能让某个分片承受明显更高的写入压力。这就是热点分片问题。因此分片键设计需要同时考虑数据分布 写入压力 查询条件 业务事务边界 未来扩容如果只考虑当前数据量分布却忽略实际查询方式可能会导致分库以后大量 SQL 都需要跨分片执行。九、分库分表之后为什么查询反而可能变慢分库分表可以把数据分散到多个存储单元但也会改变原有的查询方式。假设原来只有一张orders表SELECT*FROMordersWHEREstatusPAIDORDERBYcreated_atDESCLIMIT20;如果数据都在同一个数据库中MySQL 可以根据索引和执行计划完成查询与排序。但现在订单被分散到四个数据库。order_db_0 order_db_1 order_db_2 order_db_3而查询条件只有status PAID没有分片键user_id。那么系统通常无法直接知道目标订单在哪一个分片。可能需要Order Service | v Query Router | ---------------------------- | | | v v v order_db_0 order_db_1 order_db_2 | | | ---------------------------- | order_db_3 | v 合并查询结果 | v 全局排序实际执行可以并行访问四个分片再合并它们返回的结果。这种方式通常称为 Scatter-Gather分散查询与汇总。1. 全局排序为什么更复杂每个数据库可以分别查出自己的最新订单。例如Shard 0 订单 A 订单 D 订单 GShard 1 订单 B 订单 E 订单 HShard 2 订单 C 订单 F 订单 I但用户想看到的是整个系统按created_at排序的最新 20 条订单。所以应用或查询代理还需要合并多个分片中的结果得到全局排序。对于简单的 Top-N 查询可以让各分片分别返回候选结果再进行合并。但对于复杂的过滤、聚合和深分页执行成本可能更高。2. COUNT 和 JOIN 也可能变复杂例如SELECTCOUNT(*)FROMorders;如果订单分布在四个分片中就需要汇总各分片的统计结果或者维护相应的汇总数据。再比如SELECT*FROMorders oJOINpayments pONo.idp.order_id;如果订单与支付数据位于不同数据库原来的一条本地 JOIN 就可能变成跨服务或跨分片的数据关联。这样会带来更多网络请求、数据搬运和一致性问题。因此分库分表不是让所有 SQL 自动变快而是用更复杂的数据访问与路由机制换取更大的整体存储和处理容量。十、为什么分库分表后事务也变复杂了前面我们学习 MySQL 事务时可以使用STARTTRANSACTION;UPDATEinventorySETstockstock-1WHEREproduct_id1001ANDstock0;INSERTINTOorders(...);COMMIT;只要这些操作位于同一个可参与事务的 MySQL 实例中就可以通过本地事务协调原子提交。但假设分库以后Order Service | v order_db_0而库存位于Inventory Service | v inventory_db那么一次下单涉及两个独立的数据存储单元。创建订单 | v order_db_0 扣减库存 | v inventory_db此时order_db_0 COMMIT并不会自动让inventory_db COMMIT和它组成一个普通的本地原子事务。例如order_db_0 订单创建成功但inventory_db 库存扣减失败就需要业务层处理跨系统状态协调。这也解释了为什么前面学习分布式事务时会接触2PC TCC Saga Transactional Outbox这些机制分别适用于不同的事务协调与可靠消息场景。需要注意分库分表后并不是所有事务都会变成分布式事务。例如按照user_id分片后如果一个用户的订单主记录与订单明细都在同一个数据库分片中那么相关操作仍然可以在该分片内使用普通 MySQL 本地事务。因此好的分片设计不只是把数据均匀分散还应该尽量让经常需要一起提交的业务数据保持在可管理的事务边界内。当然如果业务天然需要跨多个分片修改数据就不能依靠一个简单的本地事务解决全部问题。十一、为什么分片数量增加后数据迁移会很麻烦前面我们使用shard_id user_id % 4把数据分配到四个分片中。假设几年后四个分片容量都逐渐不足需要扩容到八个分片。一个很直接的想法是把规则改成shard_id user_id % 8但问题马上出现。例如user_id 892原来892 % 4 0所以数据保存在order_db_0扩容后892 % 8 4按照新规则应用会去order_db_4查询。但老数据实际上还在order_db_0如果只是修改路由规则却没有迁移数据就可能出现数据实际存在 但是应用查不到的问题。这就是简单取模分片在扩容时面临的一个典型难点。1. 扩容不是简单增加四台数据库扩容时可能需要考虑旧分片中的数据怎么迁移 迁移过程中还能不能写入 新旧路由规则如何兼容 迁移期间新增的数据保存在哪里 如何验证数据没有丢失或重复 什么时候切换到新的分片规则例如旧路由user_id % 4 | v 数据迁移 | v 新路由user_id % 8如果迁移期间用户仍然不断创建订单就需要处理持续产生的新数据。不能只复制一次旧表然后直接切换。2. 为什么有些系统不直接按数据库数量取模因为user_id % N直接把路由规则绑定到了分片数量 N。一旦 N 变化部分数据的目标位置也会变化。为了降低扩容时的数据迁移范围系统可以引入额外的逻辑分片层或路由映射关系。例如user_id | v 逻辑分片编号 | v 物理数据库实例这样业务数据先映射到相对稳定的逻辑分片再由配置决定每个逻辑分片位于哪个物理数据库实例。扩容时可以将部分逻辑分片迁移到新的物理实例而不是简单修改所有数据的取模规则。不过这会引入新的路由管理和迁移复杂度。因此分库分表不能只考虑第一次拆分是否简单还应该考虑后续扩容和数据迁移的成本。十二、主从复制和分库分表可以一起使用吗可以而且这两种机制解决的问题不同。例如我们已经把订单数据分成两个分片order_db_0 order_db_1每个分片内部都可以再设置主从复制Order Service | Shard Router | -------------------- | | v v order_db_0 order_db_1 | | ---------- ---------- | | | | v v v v Primary 0 Replica 0 Primary 1 Replica 1其中分片路由负责决定某条订单数据属于哪个分片。Primary / Replica负责对应分片内部的数据复制与读取分担。例如用户 888 查询订单user_id 888 | v 定位 Shard 0 | v 选择 Primary 0 或 Replica 0这里实际上有两个独立的路由决策第一步确定数据所在的分片。第二步确定这个分片中的读写目标实例。这种架构可以同时扩展数据容量和部分读请求处理能力。但它的复杂度也明显提高。系统不再只有一个 Primary 和几个 Replica而是可能有多个分片每个分片各自维护复制关系、故障切换和监控。所以水平分片提高的是整体可扩展上限但也增加了需要管理的数据库实例数量与运维复杂度。十三、一个完整例子订单系统如何从单库逐步演进现在把前面所有知识放到同一个业务场景中。假设我们正在开发一个电商订单系统。阶段一单台 MySQL系统刚上线时Order Service | v MySQL所有订单读写都在一台数据库完成。此时最重要的是设计合理的主键和联合索引。正确使用事务和条件更新。避免不必要的全表扫描。建立备份和基本监控。对于业务量不大的系统这种方式可能已经足够。阶段二增加 Redis 缓存随着商品详情和部分订单列表的查询频率增加可以对适合缓存的数据使用 Redis。Order Service | ---- Redis | ---- MySQL这样能够减少部分重复读取。但关键订单状态、库存判断等仍然需要遵循对应的权威数据和一致性要求。阶段三增加 Replica进行读写分离当普通查询压力已经明显影响主库时Order Service | ---- Primary | ---- Replica 1 | ---- Replica 2将能够容忍短暂延迟的查询分配给 Replica。同时对于刚完成支付、刚修改订单状态等需要立即读取最新结果的请求使用主库或者具有明确复制进度等待的读取策略。这一步解决的是读压力分担不是直接解决写入瓶颈。阶段四按照业务职责拆分数据所有权随着业务与团队规模扩大User Service → user_db Order Service → order_db Payment Service → payment_db Inventory Service → inventory_db各业务服务拥有更加清晰的数据管理边界。但跨服务查询和事务需要通过接口、事件或其他协调机制完成。阶段五对高增长数据进行水平分片如果orders的数据量和写入压力仍然持续增长Order Service | v Sharding Router | ---- order_db_0 | ---- order_db_1 | ---- order_db_2 | ---- order_db_3这时需要认真设计分片键、路由、跨分片查询与扩容迁移。不过这五个阶段并不是所有公司都必须经历的固定流程。有些系统只需要单库加索引优化就能长期稳定运行。有些系统主要是读压力大增加 Replica 即可。还有一些系统可以通过历史数据归档、业务表优化或数据库实例升级延缓分片需求。架构演进应该由真实瓶颈驱动而不是根据用户量或表记录数机械套用某个方案。十四、总结数据库扩展的核心是分清读、写和数据边界这一篇从单台 MySQL 的瓶颈出发逐步介绍了主从复制、读写分离和分库分表。它们可以概括为三个层次。第一层主从复制Primary | ---- Replica 1 | ---- Replica 2核心价值是提供数据副本为读扩展和故障恢复提供基础。但异步复制存在复制延迟故障切换也可能存在数据丢失窗口。第二层读写分离写入 → Primary 部分读取 → Replica核心价值是分担读取压力。但不能简单认为增加 Replica 就能提高单主库的写入吞吐量。对于要求读后写一致性的业务还需要考虑读取主库或者等待副本追上指定的复制进度。第三层分库分表业务数据拆分 | ---- 垂直拆分 | ---- 水平拆分垂直拆分主要用于划分业务数据边界。水平拆分主要用于将同类数据和访问压力分布到多个分片。但分片会引入分片键设计 数据路由 跨分片查询 全局排序 跨库事务 数据迁移 故障恢复等额外复杂度。最后用一张表总结三种方案对比主从复制读写分离水平分库分表核心目的保留数据副本分担读取压力分散数据和写入压力能否直接扩展单主写入能力否否可以分散不同分片的写入是否存在一致性问题存在复制延迟可能读取旧数据存在跨分片一致性问题是否改变数据路由一般不改变分片归属需要选择读写目标需要确定数据所在分片主要复杂度复制、故障切换读写路由、读后写一致性路由、事务、查询、扩容迁移其中最重要的结论是主从复制并不是分库分表读写分离也不是把写请求分散到多个主库。主从复制让系统拥有数据副本。读写分离利用这些副本承担部分读取。分库分表则进一步改变数据的存储和访问边界。对于实际系统应该先检查 SQL、索引与数据库负载再根据读压力、写压力、容量和可用性要求选择相应方案。数据库扩展不是单纯增加 MySQL 的数量而是重新设计数据应该存在哪里、请求应该访问哪里以及发生故障时怎样保持正确性。到这里MySQL 的三篇核心内容就已经完整了第八篇解释事务、MVCC 和锁解决并发正确性问题。第九篇解释 B 树、回表和联合索引解决数据访问效率问题。第十篇解释主从复制、读写分离和分库分表解决数据库规模增长后的扩展问题。下一篇将进入分布式事务讨论为什么 MySQL 的本地事务在跨服务场景中不够用以及 Saga 和 Transactional Outbox 为什么会成为常见的解决方案。
阅读完成 · 觉得有帮助?
咨询建站