Skip to content

高可用与扩容:主从、分库分表的决策框架

本文是 MySQL 系统学习系列的 L3 实战篇。前置:索引的本质:B+ 树、页与查找过程。 学完可以配合面试题食用:13-master-slave-replication-and-delay-investigation14-sharding-sphere-vs-mycat26-replication-async-semisync-modes

单机扛不住了,怎么办

一个 MySQL 实例跑所有流量,迟早会碰到天花板。天花板来自三个方向:

  • 单点故障:机器挂了,整个服务不可用
  • 写入瓶颈:单机写入吞吐有限,主库的磁盘 IO 和 CPU 先到顶
  • 数据量过大:单表几千万行,B+ 树四层五层,写入变慢

这三个问题对应三类解法:主从复制(高可用)、读写分离(读扩展)、分库分表(写扩展)。但解法不是越复杂越好——很多团队在单表 500 万行时就急着拆库,结果引入分布式事务和跨分片 join 的坑,比没拆之前更痛苦。

高可用:从异步复制到 MGR

MySQL 的内置复制是搭建高可用的基础。同一个 binlog 同步到多个节点,主挂了切一个从上来。

mermaid
flowchart LR
    A[主库<br/>写入] -->|binlog| B[IO线程]
    B -->|relay log| C[SQL线程]
    C --> D[从库<br/>应用]
    D -->|binlog| E[下一级从库]

三种复制模式对应不同的 RPO(丢多少数据):

异步复制(默认):主库写 binlog 就返回,不确认从库有没有收到。如果主库崩溃时 binlog 还没传到从库,数据就丢了。RPO > 0,适合对数据一致性要求不高的场景(日志、非关键统计)。

半同步复制:主库等至少一个从库确认收到 binlog 后才提交。需要在主库安装 rpl_semi_sync_master、从库安装 rpl_semi_sync_slave 插件。RPO = 0(至少一个从库完整),但写入延迟会增大,因为多了网络往返。如果所有从库超时,自动降级回异步。

MGR(Group Replication):基于 Paxos 的多数派共识。写入需要集群内多数节点确认,Paxos 保证数据一致性。单点故障自动切换,不用外部仲裁工具。缺点是写入延迟更大(至少三次消息往返),且要求表必须有主键。

选型建议:核心业务用半同步,容忍 1-2 秒写入延迟;需要自动故障转移且写入量不大的用 MGR;异步复制只用在非关键链路。

主从延迟:一根硬骨头

即使半同步保证了 binlog 不丢,从库应用 binlog 仍然有延迟。一个常见场景:业务写入后立刻读从库,读不到刚写的数据。

延迟来源:

  • 单线程 SQL 线程(5.6 及之前):主库写入速度远超从库回放速度
  • 大事务:一个 DELETE FROM huge_table 在主库上跑 10 秒,从库也要跑 10 秒,期间全部事务堆积
  • DDL 加锁ALTER TABLE 在主库瞬间完成,从库执行时可能因为 MDL 锁争用被卡住

MySQL 5.7 引入的并行复制解决了核心瓶颈:slave_parallel_workers 开多线程,slave_parallel_type=LOGICAL_CLOCK 让同一组事务内并行回放。8.0 的 WRITESET 方式更进一步,只要事务修改的行集不冲突就能并行。

业务侧缓解延迟三板斧:

  • 写后读强制走主库:用户提交后的跳转页,读自己的数据走主库
  • 缓存兜底:写后 Redis 写一份,读从库异常时读缓存
  • 延迟监控告警Seconds_Behind_Master 虽然不精确(从库心跳间隔决定),但仍然是运维最直接的指标

分库分表:什么时候真需要

分库分表是运维复杂度最高的解法,不是因为代码难写,是因为跨分片修改的代价是线性的

先问自己三个问题:

  1. 单表行数超过 2000 万了吗?—— 没有就先缓冲
  2. 写入吞吐超过单机 IOPS 上限了吗?—— 没有就先优化
  3. 索引、归档、读写分离都试过了吗?—— 没有就先试

垂直拆分(按业务):把用户表、订单表、商品表拆到不同的库。这个业务上天然隔离,相对简单,但跨库事务变成分布式事务(XA 或 TCC),跨库 join 得靠应用层或数据汇总。

水平拆分(按行):同一个表按某种规则分散到多个实例。触发线通常看三个指标同时接近上限:单表行数(2000 万+)、单库磁盘(500GB+)、写入 tps(2000+)。

分片键与基因法

分片键的选择关系到后续所有的查询路由。选 user_id 分片,订单查询按订单号就得广播。

基因法解决这个问题:order_id 里编码 user_id 的分片信息。

java
public class ShardingGene {
    // 假设分 16 库(4 位二进制)
    private static final int DB_COUNT = 16;
    private static final int GENE_BITS = 4;

    /**
     * 生成带分片基因的订单 ID
     * @param userId 用户 ID
     * @param seq 自增序列
     * @return 订单 ID(高位+低位)
     */
    public static long generateOrderId(long userId, long seq) {
        // 取 user_id 低 4 位作为分片基因
        long gene = userId & (DB_COUNT - 1);
        // 序列左移 4 位,嵌入基因,高位留时间戳位
        return (seq << GENE_BITS) | gene;
    }

    /**
     * 从订单 ID 还原分片编号
     * @param orderId 订单 ID
     * @return 分片编号 0-15
     */
    public static int shardByOrderId(long orderId) {
        return (int) (orderId & (DB_COUNT - 1));
    }

    /**
     * 从 user_id 也得到同样的分片编号
     */
    public static int shardByUserId(long userId) {
        return (int) (userId & (DB_COUNT - 1));
    }
}

这样,不管是按 order_id 还是按 user_id 查询,都能直接定位到同一台分片,不用广播。

扩容时如果分片数倍增(16→32),基因位数从 4 变 5,历史数据必须迁移。这时候有两种策略:

  • 双写迁移:旧库写入的同时往新库写一份,读走旧库,经过一段时间后切换读
  • 影子表同步:新库和旧库保持同步,数据一致后切换

后者更安全,但需要额外的同步工具(如 Canal + 自定义消费者)。

ShardingSphere 引入的成本

中间件让分库分表看起来"透明",但代价不是开发阶段付的,是运维阶段。

sql
-- 跨分片查询:分页+排序,每片取 N 条,归并排序后再取
-- 每片数据量越大,归并开销越大
SELECT * FROM orders WHERE status = 'paid' ORDER BY create_time DESC LIMIT 20;

-- 跨分片事务:引入 XA 或 Seata AT,性能下降明显
-- 业务上应尽量避免跨分片事务

引入 ShardingSphere 前对照清单:

  • 跨分片查询必须带上分片键 —— 否则全分片扫描
  • 分页越深越慢 —— 需要改写为游标分页
  • 分布式事务每笔增加 2-5ms 延迟
  • 升级中间件版本需要全链路回归
  • 所有分片的路由规则变更需要重新规划数据迁移

常见误区与小结

  • 误区 1:复制延迟只看 Seconds_Behind_Master。这个值基于从库心跳时间,大事务期间可能不准。配合 SHOW SLAVE STATUSRetrieved_Gtid_SetExecuted_Gtid_Set 比较更可靠。
  • 误区 2:半同步复制保证不丢数据。它保证的是"至少一个从库收到了 binlog",但如果主库和收到 binlog 的从库同时宕机,数据仍然可能丢。多数派方案(MGR/MySQL Cluster)才能做到真正的 RPO=0。
  • 误区 3:分库分表后查询变慢是中间件的问题。大部分情况是分片键没选好,导致查询变成全分片广播。先确认 WHERE 条件是否匹配分片键。
  • 误区 4:分库分表能解决所有性能问题。分库分表解决的是写入扩展和单库容量,查询性能反而可能下降(跨分片 join/聚合/排序)。读性能问题优先用读写分离+缓存。
  • 误区 5:扩容就是加机器。加机器后需要重新分片,数据迁移是重活。扩容前先评估是否能用索引优化、归档历史数据、或者冷热分离来解决。

小结:高可用和扩展是一个阶梯决策——先确认是否需要跨出当前层级。单机扛不住先看索引和慢 SQL,然后是主从+读写分离,再是垂直拆分,最后才是水平拆分。每一步都增加运维复杂度,确保每层的方案能解决当前的问题再做下一步。

下一篇将进入设计模式模块,从系统学习转向常见模式的设计思路与代码实现。

参考

参考:MySQL 官方手册《Replication》、ShardingSphere 官方文档《Sharding-Proxy 架构》、丁奇《MySQL 实战 45 讲》

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1