31、MySQL复制机制:主从复制原理、二进制日志、GTID复制、C语言监控复制状态

MySQL的主从复制,说白了就是让一台MySQL服务器(主库)的数据,自动同步到另一台或多台服务器(从库)上。这玩意儿在咱们生产环境中几乎是标配。我这些年带团队做项目,只要涉及高可用、读写分离或者数据备份,第一反应就是先把主从复制搭起来。

你可能会问:「为啥不直接用程序双写?」嗯,双写的问题在于,一旦某个节点挂了,数据一致性就崩了。而MySQL的复制机制,是经过十几年打磨的成熟方案,靠谱得多。

31.1 主从复制原理:日志驱动的数据同步

主从复制的核心,其实就三个线程在干活:

  • 主库的Binlog Dump线程:负责把二进制日志(Binary Log)推送给从库
  • 从库的I/O线程:负责接收主库发来的日志,并写入到本地的中继日志(Relay Log)
  • 从库的SQL线程:负责读取中继日志,并重放里面的SQL语句

整个流程就像流水线:主库写Binlog → 从库I/O线程拉过来写Relay Log → 从库SQL线程执行。我刚开始接触时觉得这设计挺笨的,后来才发现,这种解耦方式让复制变得异常稳定——哪怕网络抖动,I/O线程断了,SQL线程还能继续执行已经收到的日志。

核心要点:主从复制是异步的。主库提交事务后,不会等从库确认。这意味着从库的数据可能会有延迟,但主库的性能不受影响。

主库 (Master) Binlog Dump 线程 写入 Binary Log 从库 (Slave) I/O 线程 写入 Relay Log SQL 线程 重放 Relay Log 推送 Binlog 读取并执行

31.2 二进制日志:复制的基石

二进制日志(Binlog)是MySQL复制的基础。主库上所有数据变更,都会记录到Binlog里。我见过不少新手,一上来就开主从复制,结果发现从库数据对不上——查了半天,原来是Binlog没开。

Binlog有三种格式,我列个表给你看:

格式 说明 优缺点
STATEMENT 记录原始SQL语句 日志量小,但某些函数(如NOW())会导致主从不一致
ROW 记录每一行数据的变化 最安全,但日志量大。我推荐生产环境用这个
MIXED 混合模式,默认用STATEMENT,必要时自动切ROW 折中方案,但偶尔会有坑

我的建议:生产环境直接用ROW格式。虽然日志量大一点,但数据一致性有保障。我曾经在STATEMENT格式下吃过亏——一个UPDATE语句用了LIMIT,主库和从库更新的行数不一样,排查了一整天才发现是Binlog格式的问题。

开启Binlog很简单,在my.cnf里加上:

[mysqld]
log-bin=mysql-bin
binlog-format=ROW
server-id=1   # 主从的server-id必须不同

31.3 GTID复制:让复制更简单

传统的基于文件名+位置的复制,有个烦人的问题:如果主库挂了,你得手动去找从库同步到了哪个Binlog文件的哪个位置。一旦搞错,数据就乱了。

GTID(Global Transaction Identifier)就是来解决这个问题的。每个事务都有一个全局唯一的ID,格式像这样:server_uuid:transaction_id。从库只需要记住自己执行到了哪个GTID,主库挂了换一台,自动就能接上。

启用GTID也很简单:

[mysqld]
gtid_mode=ON
enforce_gtid_consistency=ON

我个人习惯,新项目一律用GTID复制。它最大的好处是——故障切换时,你不需要去关心「从库的日志位置对不对」,GTID会自动帮你搞定。我记得有一次线上主库磁盘满了,我们切到备库,整个过程不到30秒,GTID功不可没。

31.4 C语言监控复制状态

好了,理论说完了,咱们来点实际的。怎么用C语言监控MySQL的复制状态?

核心思路很简单:连接从库,执行 SHOW SLAVE STATUS,然后解析结果。关键字段就两个:

  • Slave_IO_Running:I/O线程是否在跑
  • Slave_SQL_Running:SQL线程是否在跑
  • Seconds_Behind_Master:从库落后主库多少秒

下面是一个完整的C语言示例,用MySQL Connector/C实现:

#include <mysql/mysql.h>
#include <stdio.h>
#include <string.h>

int check_slave_status(const char *host, const char *user,
                       const char *pass, const char *db) {
    MYSQL *conn = mysql_init(NULL);
    if (conn == NULL) {
        fprintf(stderr, "mysql_init failed\n");
        return -1;
    }

    // 连接MySQL
    if (mysql_real_connect(conn, host, user, pass, db, 3306, NULL, 0) == NULL) {
        fprintf(stderr, "连接失败: %s\n", mysql_error(conn));
        mysql_close(conn);
        return -1;
    }

    // 执行 SHOW SLAVE STATUS
    if (mysql_query(conn, "SHOW SLAVE STATUS")) {
        fprintf(stderr, "查询失败: %s\n", mysql_error(conn));
        mysql_close(conn);
        return -1;
    }

    MYSQL_RES *result = mysql_store_result(conn);
    if (result == NULL) {
        fprintf(stderr, "获取结果失败: %s\n", mysql_error(conn));
        mysql_close(conn);
        return -1;
    }

    MYSQL_ROW row = mysql_fetch_row(result);
    if (row == NULL) {
        printf("该服务器不是从库,或复制未启动\n");
        mysql_free_result(result);
        mysql_close(conn);
        return 0;
    }

    // 获取字段索引
    unsigned int num_fields = mysql_num_fields(result);
    MYSQL_FIELD *fields = mysql_fetch_fields(result);

    char *io_running = NULL;
    char *sql_running = NULL;
    char *seconds_behind = NULL;

    for (unsigned int i = 0; i < num_fields; i++) {
        if (strcmp(fields[i].name, "Slave_IO_Running") == 0)
            io_running = row[i];
        else if (strcmp(fields[i].name, "Slave_SQL_Running") == 0)
            sql_running = row[i];
        else if (strcmp(fields[i].name, "Seconds_Behind_Master") == 0)
            seconds_behind = row[i];
    }

    printf("=== 复制状态监控 ===\n");
    printf("I/O线程状态: %s\n", io_running ? io_running : "NULL");
    printf("SQL线程状态: %s\n", sql_running ? sql_running : "NULL");
    printf("落后主库时间: %s 秒\n", seconds_behind ? seconds_behind : "NULL");

    // 判断复制是否正常
    if (io_running && strcmp(io_running, "Yes") == 0 &&
        sql_running && strcmp(sql_running, "Yes") == 0) {
        printf("✅ 复制运行正常\n");
    } else {
        printf("❌ 复制异常!请检查\n");
    }

    mysql_free_result(result);
    mysql_close(conn);
    return 0;
}

int main() {
    // 替换成你的从库连接信息
    check_slave_status("192.168.1.100", "monitor", "password", "");
    return 0;
}

注意:监控账号只需要 REPLICATION CLIENT 权限,不要给超级权限。我曾经见过有人用root账号做监控,结果被黑客利用,整个库被删了。权限最小化原则,任何时候都不能忘。

31.5 实战中的避坑指南

做复制监控这几年,我踩过不少坑,挑几个典型的说说:

  • 网络超时问题:C语言连接MySQL时,默认超时时间很长。如果从库挂了,你的监控程序会卡住。建议设置连接超时:mysql_options(conn, MYSQL_OPT_CONNECT_TIMEOUT, (const char *)&timeout);
  • Seconds_Behind_Master 为 NULL:这通常意味着SQL线程没在跑,或者I/O线程断了。别只看这个字段,要结合I/O和SQL线程一起判断。
  • GTID 模式下的事务跳跃:如果你手动在从库执行了写操作,GTID会乱掉。我曾经手贱在从库上跑了个UPDATE,结果复制直接停了。记住:从库永远只读!

嗯,关于MySQL复制,今天就聊到这儿。这些内容都是我这些年摸爬滚打总结出来的,希望能帮你少走弯路。复制机制本身不复杂,但细节决定成败——Binlog格式选对、GTID开起来、监控写扎实,这三件事做好了,你的数据库高可用就稳了一大半。


公众号:蓝海资料掘金营,微信deep3321