跳到主要内容
预计阅读 39 分钟

数据的分身术 —— 主从复制与高可用方案

前面十章我们搞清楚了单台MySQL的内部机制——数据如何存储、如何索引、如何保障事务安全。但一台服务器总有它的极限:硬盘坏了怎么办?读请求暴涨扛不住怎么办?今天我们来看看如何让数据拥有”分身”,以及如何让系统在故障面前屹立不倒。

📋 开篇自测:你已经知道多少?

  1. MySQL主从复制依赖的核心日志是什么?它和redo日志有什么区别?
  2. binlog有几种格式?各自的优缺点是什么?
  3. 什么是GTID?它解决了传统复制的什么痛点?
  4. 主库挂了之后,从库如何”接班”?有哪些自动化方案?

一、为什么需要复制——单点的脆弱

想象一家医院只有一份病历档案,没有副本。一旦档案室失火,所有患者记录付之一炬。数据库也是同样的道理:只要数据仅存在于一台机器上,这台机器就是整个系统的”命门”。

主从复制要解决的核心问题有三个:

  1. 数据冗余:多台机器上都有数据的副本,一台挂了不至于丢数据
  2. 读扩展:把读请求分散到多台从库,减轻主库压力
  3. 故障切换:主库不可用时,能快速将从库提升为新主库

🤔 想一想 你维护的业务系统中,读请求和写请求的比例大约是多少?如果读请求占80%以上,主从复制带来的读扩展效果会怎样?


二、复制的基本原理——三个线程的接力赛

MySQL的主从复制本质上是一场日志的接力传递,由三个角色协同完成:

主库(Master)                          从库(Slave/Replica)
┌────────────────┐                    ┌────────────────────────┐
│  客户端写入     │                    │                        │
│      ↓         │                    │                        │
│  生成binlog    │                    │                        │
│      ↓         │    网络传输         │                        │
│  Binlog Dump   │ ──────────────→    │  I/O Thread            │
│  Thread        │    (binlog事件)     │      ↓                 │
│                │                    │  写入Relay Log          │
│                │                    │      ↓                 │
│                │                    │  SQL Thread             │
│                │                    │      ↓                 │
│                │                    │  重放到从库数据          │
└────────────────┘                    └────────────────────────┘

第一棒:主库的Binlog Dump线程

当从库连接到主库请求复制时,主库会启动一个Binlog Dump线程,负责读取本地的binlog文件并将事件发送给从库。每当主库有新的写操作产生binlog,这个线程就会把新的事件推送出去。

第二棒:从库的I/O线程

从库的I/O线程负责接收主库发来的binlog事件,并写入本地的中继日志(Relay Log)。中继日志的格式和binlog完全相同,只是换了个名字和存储位置。

第三棒:从库的SQL线程

从库的SQL线程读取中继日志中的事件,逐条在从库上执行(重放),从而使从库的数据与主库保持一致。

-- 在从库上查看复制线程的状态
SHOW SLAVE STATUS\G
-- MySQL 8.0.22+ 推荐使用新语法
SHOW REPLICA STATUS\G

关键字段解读:

字段含义
Slave_IO_RunningI/O线程是否在运行
Slave_SQL_RunningSQL线程是否在运行
Seconds_Behind_Master从库落后主库的秒数
Master_Log_File当前正在读取的主库binlog文件
Read_Master_Log_Pos已读取到的binlog位置
Relay_Master_Log_FileSQL线程正在执行的对应主库binlog文件
Exec_Master_Log_PosSQL线程已执行到的binlog位置

⚠️ 常见误区 误区:Seconds_Behind_Master = 0就代表主从完全同步。 这个值只反映SQL线程的重放进度,不包含I/O线程的网络传输延迟。在网络抖动的情况下,即使这个值为0,从库数据仍可能有短暂的滞后。更可靠的做法是对比主库和从库的GTID执行集合。


三、binlog的三种格式——复制的信息载体

binlog是复制的核心载体。它有三种格式,各有取舍。

Statement格式

记录原始的SQL语句。

-- 查看或设置binlog格式
SHOW VARIABLES LIKE 'binlog_format';
SET GLOBAL binlog_format = 'STATEMENT';

优点是日志量小。一条UPDATE inventory SET quantity = quantity - 1 WHERE product_id < 10000只记录这一条SQL,几十个字节。

缺点是某些SQL在从库上重放的结果可能与主库不一致。最典型的情况:

-- 这条语句在主库和从库上可能删除不同的行
-- 因为没有ORDER BY,LIMIT的结果取决于执行计划
DELETE FROM access_log WHERE create_time < '2025-01-01' LIMIT 1000;

-- 使用了不确定函数
INSERT INTO events (event_id, happened_at) VALUES (UUID(), NOW());
-- UUID()在主从上产生不同的值

Row格式

不记录SQL语句,而是记录每一行数据修改前后的值。

SET GLOBAL binlog_format = 'ROW';

优点是绝对精准——主库改了哪一行的哪个字段从什么值变成什么值,全部记录在案。不存在Statement格式的不确定性问题。

缺点是日志量可能很大。同样那条UPDATE inventory SET quantity = quantity - 1 WHERE product_id < 10000,如果影响了5000行,Row格式会记录5000条行变更记录,每条都包含修改前后的完整行数据。

# 用mysqlbinlog工具查看Row格式的binlog内容
mysqlbinlog --base64-output=DECODE-ROWS -v binlog.000003

Mixed格式

MySQL自动判断:对于能安全使用Statement格式的SQL用Statement,对于可能导致不一致的SQL自动切换为Row。

SET GLOBAL binlog_format = 'MIXED';

看起来很美好,但实际上MySQL对”哪些SQL是安全的”判断并不完美,偶尔仍会出现主从不一致。

生产建议

强烈推荐使用Row格式。 MySQL 8.0已将Row设为默认值。虽然日志量大一些,但数据一致性是最重要的。磁盘空间不够可以加磁盘,数据不一致却可能带来灾难性后果。

# my.cnf 推荐配置
[mysqld]
binlog_format = ROW
binlog_row_image = FULL    # 记录完整行数据(默认值)

binlog_row_image有三个选项:FULL(记录所有列)、MINIMAL(只记录被修改的列和识别行所需的列)、NOBLOB(不记录未修改的BLOB/TEXT列)。生产环境建议保持FULL,便于数据恢复和审计。

🤔 想一想 如果你的业务经常执行批量UPDATE(一次更新几万行),使用Row格式会带来什么问题?你会如何权衡和优化?


四、复制拓扑——分身的阵型

根据业务场景的不同,主从复制有多种拓扑结构。

一主多从

最常见的拓扑。一个主库带多个从库。

        ┌──────┐
   ┌───→│ 从库A │  ← 读请求
   │    └──────┘
┌──┴──┐ ┌──────┐
│ 主库 │→│ 从库B │  ← 读请求
└──┬──┘ └──────┘
   │    ┌──────┐
   └───→│ 从库C │  ← 报表/备份
        └──────┘

适用场景:读多写少的业务(如内容网站、电商商品展示)。不同从库还可以承担不同职责——从库A/B分担线上读请求,从库C专门跑报表查询或做数据备份,避免大查询拖慢线上服务。

级联复制

从库不直接连主库,而是连接另一个从库获取数据。这种结构可以减轻主库的Binlog Dump压力。

┌──────┐    ┌──────┐    ┌──────┐
│ 主库  │───→│ 从库A │───→│ 从库B │
└──────┘    └──┬───┘    └──────┘
               │        ┌──────┐
               └───────→│ 从库C │
                        └──────┘

从库A开启log_slave_updates(或MySQL 8.0+的log_replica_updates),将自己从主库收到的binlog事件写入自己的binlog,再转发给下游从库。

-- 从库A的配置
SHOW VARIABLES LIKE 'log_replica_updates';
-- 需要设为ON,从库才会将复制的事件写入自己的binlog

双主(互为主从)

两台MySQL互相复制。通常只有一台对外提供写服务,另一台作为热备。

┌──────┐  ←──binlog──→  ┌──────┐
│主库A  │               │主库B  │
│(活跃) │  ←──binlog──→  │(备用) │
└──────┘                └──────┘

双主最大的坑是写冲突:如果两边同时修改同一行数据,可能导致数据不一致。因此生产中通常只让一边写,另一边只读。双主更多是为了加速故障切换——备用主库已经有完整数据且复制链路就绪,切换时只需将写流量指过去。

避免主键冲突的常用手段:

-- 主库A使用奇数ID
SET GLOBAL auto_increment_offset = 1;
SET GLOBAL auto_increment_increment = 2;
-- 生成的ID:1, 3, 5, 7, ...

-- 主库B使用偶数ID
SET GLOBAL auto_increment_offset = 2;
SET GLOBAL auto_increment_increment = 2;
-- 生成的ID:2, 4, 6, 8, ...

五、GTID复制——给每笔交易一张身份证

传统复制依赖binlog文件名和位置(如binlog.000003的第4567字节位置)来定位同步进度。这种方式有几个麻烦:

  • 主库切换后,新主库的binlog文件名和位置与旧主库不同,从库需要手动找到对应位置
  • 级联复制中,中继日志的位置和主库binlog的位置需要反复换算
  • 排查问题时难以确定某个事务是否已经在从库上执行过

GTID(Global Transaction Identifier,全局事务标识符) 就是为了解决这些问题。每个事务都有一个全局唯一的ID:

GTID = source_id:transaction_id
例如:3E11FA47-71CA-11E1-9E33-C80AA9429562:42

source_id是主库的server_uuid(自动生成的唯一标识),transaction_id是一个递增的序号。

开启GTID

# my.cnf(主库和所有从库都需要配置)
[mysqld]
gtid_mode = ON
enforce_gtid_consistency = ON
log_bin = mysql-bin
log_replica_updates = ON

配置GTID复制

-- 在从库上配置复制(使用GTID自动定位)
CHANGE REPLICATION SOURCE TO
    SOURCE_HOST = '192.168.1.10',
    SOURCE_USER = 'repl_user',
    SOURCE_PASSWORD = 'repl_pass',
    SOURCE_AUTO_POSITION = 1;

-- 启动复制
START REPLICA;

-- 查看复制状态
SHOW REPLICA STATUS\G

SOURCE_AUTO_POSITION = 1是关键参数。开启后,从库会告诉主库”我已经执行过哪些GTID”,主库自动从下一个未执行的事务开始发送。不需要手动指定binlog文件名和位置。

GTID的执行集合

-- 查看主库已执行的所有GTID
SHOW VARIABLES LIKE 'gtid_executed';
-- 结果示例:3E11FA47-71CA-11E1-9E33-C80AA9429562:1-1542

-- 查看从库已执行的GTID
SHOW VARIABLES LIKE 'gtid_executed';
-- 如果和主库一致,说明完全同步

GTID让主从切换变得简单——从库只需要知道新主库的连接信息,GTID会自动对齐进度,无需手动计算binlog位置。

⚠️ 常见误区 误区:开启GTID后可以随意在从库上执行写操作。 从库上的写操作会生成独立的GTID(使用从库自己的server_uuid),导致GTID集合与主库分叉。之后如果这台从库被提升为主库,其他从库可能无法正确同步。生产环境建议在从库上设置super_read_only = ON,彻底禁止写入。


六、读写分离——让分身各司其职

有了主从复制,下一步自然是让主库专心处理写请求,从库分担读请求。这就是读写分离。

应用层路由

最简单的方式:在应用代码中根据SQL类型选择不同的数据源。

# Python伪代码示例
class DatabaseRouter:
    def __init__(self):
        self.master_pool = create_pool(host='192.168.1.10')  # 主库
        self.replica_pools = [
            create_pool(host='192.168.1.11'),  # 从库1
            create_pool(host='192.168.1.12'),  # 从库2
        ]
        self._replica_index = 0

    def get_connection(self, read_only=False):
        if read_only:
            # 轮询选择从库
            pool = self.replica_pools[self._replica_index % len(self.replica_pools)]
            self._replica_index += 1
            return pool.get_connection()
        else:
            return self.master_pool.get_connection()

# 使用
router = DatabaseRouter()

# 写操作走主库
conn = router.get_connection(read_only=False)
conn.execute("INSERT INTO orders (customer_id, total) VALUES (1001, 299.00)")

# 读操作走从库
conn = router.get_connection(read_only=True)
result = conn.execute("SELECT * FROM orders WHERE customer_id = 1001")

优点:实现简单,无额外依赖。缺点:路由逻辑分散在业务代码中,切换从库需要改代码重新部署。

代理层路由(ProxySQL)

ProxySQL是一个高性能的MySQL协议感知代理,部署在应用和MySQL之间,自动完成读写分离。

应用程序 → ProxySQL(6033端口) → 主库(写)
                              → 从库A(读)
                              → 从库B(读)

ProxySQL通过规则引擎自动判断SQL是读还是写,并路由到相应的后端MySQL实例:

-- 在ProxySQL管理端口(6032)配置后端服务器
INSERT INTO mysql_servers (hostgroup_id, hostname, port, weight)
VALUES
    (10, '192.168.1.10', 3306, 1),    -- 写组(hostgroup 10)
    (20, '192.168.1.11', 3306, 1),    -- 读组(hostgroup 20)
    (20, '192.168.1.12', 3306, 1);    -- 读组(hostgroup 20)

-- 配置读写分离规则
INSERT INTO mysql_query_rules (rule_id, match_pattern, destination_hostgroup, apply)
VALUES
    (1, '^SELECT.*FOR UPDATE$', 10, 1),   -- SELECT FOR UPDATE走主库
    (2, '^SELECT', 20, 1),                -- 普通SELECT走从库
    (3, '.*', 10, 1);                     -- 其他语句走主库

-- 加载配置使其生效
LOAD MYSQL SERVERS TO RUNTIME;
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
SAVE MYSQL QUERY RULES TO DISK;

ProxySQL还支持连接池、查询缓存、故障检测等高级功能,是生产环境中读写分离的常见选择。

读写分离的”数据延迟”陷阱

读写分离有一个绕不开的问题:主从延迟。用户刚提交了一笔订单(写主库),立刻刷新页面查看订单详情(读从库),如果从库还没同步完,用户会看不到自己的订单。

常见解决策略:

  1. 关键读走主库:刚写入后的立即查询,强制路由到主库
  2. 延迟感知:检查从库的Seconds_Behind_Master,超过阈值时将读请求回退到主库
  3. 会话一致性:同一个用户会话中,如果发生过写操作,后续的读都走主库(直到延迟消除)

七、半同步复制与组复制——更强的一致性保障

异步复制的风险

默认的异步复制中,主库提交事务后不等从库确认就返回成功。如果主库在事务提交后、binlog传输前崩溃,从库上就没有这笔事务的记录——数据丢失了。

异步复制的时间线:
  主库提交事务 → 返回客户端成功 → (binlog还没来得及发给从库) → 主库崩溃
                                                              ↑ 数据丢失!

半同步复制(Semi-Synchronous Replication)

半同步复制要求主库在提交事务后,至少等一个从库确认收到binlog,才向客户端返回成功。

半同步复制的时间线:
  主库提交事务 → 发送binlog给从库 → 等待从库ACK → 收到ACK → 返回客户端成功
-- 主库安装并启用半同步插件(MySQL 8.0.26+ 命名)
INSTALL PLUGIN rpl_semi_sync_source SONAME 'semisync_source.so';
SET GLOBAL rpl_semi_sync_source_enabled = ON;
SET GLOBAL rpl_semi_sync_source_timeout = 3000;  -- 自定义为3秒(默认值为10000ms即10秒),超时后退化为异步
-- MySQL 8.0.25 及更早版本使用旧名称:
-- INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';

-- 从库安装并启用半同步插件
INSTALL PLUGIN rpl_semi_sync_replica SONAME 'semisync_replica.so';
SET GLOBAL rpl_semi_sync_replica_enabled = ON;
-- MySQL 8.0.25 及更早版本使用旧名称:
-- INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';

-- 重启从库的I/O线程使配置生效
STOP REPLICA IO_THREAD;
START REPLICA IO_THREAD;

-- 查看半同步状态
SHOW STATUS LIKE 'Rpl_semi_sync%';

注意:半同步只保证binlog传输到了从库的中继日志,不保证从库已经执行了这些事务。因此从库数据仍然可能有短暂延迟,但至少不会丢失数据。

组复制(MySQL Group Replication / MGR)

MGR是MySQL 5.7.17引入的原生高可用方案,基于Paxos协议实现多节点数据一致性。

┌──────┐    ┌──────┐    ┌──────┐
│节点A  │←──→│节点B  │←──→│节点C  │
│(主)   │    │(从)   │    │(从)   │
└──────┘    └──────┘    └──────┘
     ↑           ↑           ↑
     └───────────┴───────────┘
          组通信层(Paxos)

MGR有两种模式:

  • 单主模式:一个节点可写,其他节点只读。主节点故障时自动选举新主节点
  • 多主模式:所有节点都可写。冲突事务通过认证(Certification)机制检测并回滚
-- 查看组复制状态
SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE
FROM performance_schema.replication_group_members;

-- 示例输出
-- +-------------+-------------+--------------+-------------+
-- | MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
-- +-------------+-------------+--------------+-------------+
-- | node-a      |        3306 | ONLINE       | PRIMARY     |
-- | node-b      |        3306 | ONLINE       | SECONDARY   |
-- | node-c      |        3306 | ONLINE       | SECONDARY   |
-- +-------------+-------------+--------------+-------------+

🤔 想一想 半同步复制中,如果唯一的从库网络断开超过rpl_semi_sync_source_timeout秒,主库会怎样处理?这对业务会有什么影响?


八、高可用架构选型——给分身装上”大脑”

光有主从复制还不够。主库挂了之后,谁来决定让哪个从库”接班”?谁来通知应用切换连接地址?这就需要高可用管理组件。

MHA(Master High Availability)

MHA是一个经典的开源方案,专注做一件事:主库故障时自动切换。需要注意的是,MHA 项目已多年未活跃维护(最后一个 release 是 2018 年),虽然目前仍有大量存量部署在运行,但对于新项目,建议优先考虑 Orchestrator 或 InnoDB Cluster 等更活跃的方案。

工作流程:

  1. MHA Manager持续监控主库的健康状态
  2. 检测到主库不可用后,从多个从库中选出数据最新的那个
  3. 对比各从库的中继日志差异,补齐数据
  4. 将选中的从库提升为新主库
  5. 其他从库重新指向新主库

MHA的切换速度通常在10-30秒。

Orchestrator

Orchestrator是GitHub开源的MySQL高可用和拓扑管理工具,功能比MHA更丰富:

  • 可视化的复制拓扑图
  • 自动故障检测和切换
  • 支持拓扑重构(比如把从库从一个主库迁移到另一个主库)
  • 提供HTTP API,便于与其他系统集成

InnoDB Cluster

MySQL官方的一站式高可用方案,由三个组件组成:

┌─────────────────────────────────────────┐
│              MySQL Shell                 │
│          (管理和配置工具)                   │
└───────────────┬─────────────────────────┘

┌─────────────────────────────────────────┐
│       MySQL Group Replication            │
│    (底层数据同步,基于Paxos)                │
│  ┌──────┐  ┌──────┐  ┌──────┐           │
│  │节点A  │  │节点B  │  │节点C  │           │
│  └──────┘  └──────┘  └──────┘           │
└───────────────┬─────────────────────────┘

┌─────────────────────────────────────────┐
│           MySQL Router                   │
│     (应用接入层,自动路由读写)               │
└─────────────────────────────────────────┘
  • MySQL Group Replication:底层数据同步引擎
  • MySQL Shell:管理和配置集群的工具
  • MySQL Router:应用层代理,自动检测集群状态并路由请求
// 使用MySQL Shell创建InnoDB Cluster
// 在MySQL Shell(JS模式)中执行
dba.configureInstance('root@node-a:3306');
dba.configureInstance('root@node-b:3306');
dba.configureInstance('root@node-c:3306');

var cluster = dba.createCluster('prod_cluster');
cluster.addInstance('root@node-b:3306');
cluster.addInstance('root@node-c:3306');

// 查看集群状态
cluster.status();

选型建议

方案复杂度数据一致性切换速度适用场景
MHA依赖半同步10-30秒存量传统主从架构(项目已停止维护)
Orchestrator依赖半同步10-30秒大规模MySQL集群管理
InnoDB Cluster强一致秒级新项目、追求官方支持

九、常见故障排查——分身术的”副作用”

主从延迟

主从延迟是最常见的复制问题。表现为Seconds_Behind_Master持续不为零或不断增大。

常见原因及排查:

-- 1. 从库的SQL线程是单线程重放,主库并发写入量大时跟不上
--    解决:开启多线程复制
SHOW VARIABLES LIKE 'replica_parallel_workers';
-- 如果为0或1,说明是单线程,建议设为4-16
SET GLOBAL replica_parallel_workers = 8;
SET GLOBAL replica_parallel_type = 'LOGICAL_CLOCK';
-- LOGICAL_CLOCK 从 MySQL 5.7.2 起可用,MySQL 8.0.27+ 已将其设为默认值
SET GLOBAL replica_preserve_commit_order = ON;

-- 2. 从库上执行了大查询(如报表分析),占用了资源
--    解决:报表查询用专用从库,与线上读库隔离

-- 3. 主库执行了大事务(如不带WHERE的UPDATE)
--    解决:大批量操作拆分为小批次
-- 不好的做法
UPDATE user_profiles SET status = 'inactive' WHERE last_login < '2024-01-01';
-- 好的做法:分批处理
UPDATE user_profiles SET status = 'inactive'
WHERE last_login < '2024-01-01' AND id BETWEEN 1 AND 10000;
-- 然后 10001-20000,以此类推

主从数据不一致

从库的数据与主库不一致,通常因为以下原因:

  1. 使用了Statement格式binlog,不确定性SQL导致不一致
  2. 在从库上直接执行了写操作
  3. 复制过程中出错被跳过(sql_slave_skip_counter

排查工具:

# 使用pt-table-checksum检测主从数据差异
pt-table-checksum --host=192.168.1.10 --user=root --password=xxx \
    --databases=app_db --tables=orders

# 使用pt-table-sync修复不一致的数据
pt-table-sync --execute --sync-to-master h=192.168.1.11,u=root,p=xxx \
    --databases=app_db --tables=orders

复制中断

-- 查看错误信息
SHOW REPLICA STATUS\G
-- 关注 Last_IO_Error 和 Last_SQL_Error 字段

-- 常见错误1:从库上已存在要插入的记录(主键冲突,错误码1062)
-- 常见错误2:从库上找不到要更新/删除的记录(错误码1032)

-- 紧急处理:跳过一条错误事件(谨慎使用!)
STOP REPLICA;
SET GLOBAL sql_slave_skip_counter = 1;
START REPLICA;

-- GTID模式下跳过特定事务
SET GTID_NEXT = '3E11FA47-71CA-11E1-9E33-C80AA9429562:42';
BEGIN; COMMIT;   -- 注入空事务
SET GTID_NEXT = 'AUTOMATIC';
START REPLICA;

⚠️ 常见误区 误区:sql_slave_skip_counter可以随意使用来跳过复制错误。 每跳过一条事件,主从数据就可能多一处不一致。频繁跳过等于在给系统埋雷。正确做法是查明错误原因,修复根本问题后再恢复复制。


📝 掌握度自测

  1. MySQL主从复制涉及哪三个线程?各自的职责是什么?
  2. binlog的Statement、Row、Mixed三种格式各有什么特点?生产环境推荐哪种?
  3. GTID解决了传统基于binlog位置的复制的什么问题?配置GTID复制需要哪些关键参数?
  4. 半同步复制与异步复制的核心区别是什么?它能保证从库数据实时与主库一致吗?
  5. 主从延迟有哪些常见原因?如何通过多线程复制来缓解?

💡 自我评估

  • 答对5题:对MySQL复制和高可用体系有了全面掌握,可以着手设计生产级别的高可用架构了
  • 答对3-4题:核心概念理解到位,建议动手搭建一个主从环境实际操作体验
  • 答对0-2题:复制是MySQL运维的重要技能,建议重点阅读binlog格式和GTID部分,这是理解复制的基础

购买课程解锁全部内容

让查询飞起来:MySQL 从索引到主从高可用

¥29.90