MySQL 是怎么保证主备一致的?
在数据库的世界里,主备同步(Master-Slave Replication)就像武侠小说里的"分身术"------主库负责写,备库负责读,一旦主库挂了,备库能立刻顶上。但这里有个灵魂拷问:备库凭什么能和主库保持一致? 如果主库刚写完一条数据,备库还没来得及同步,主库就宕机了,数据不就丢了吗?今天,咱们就用大白话拆解 MySQL 的主备同步机制,看看它是怎么做到"滴水不漏"的。---## 一、主备同步的"老剧本":binlog 和 relay logMySQL 的主备同步,核心就靠两个"记事本":- 主库的 binlog :记录所有数据变更操作(比如 INSERT、UPDATE、DELETE)。- 备库的 relay log :从主库拉取 binlog 后,先存到本地,再"回放"到备库。整个流程像一场接力赛:bash主库执行 SQL → 写入 binlog → 备库 IO 线程拉取 → 写入 relay log → 备库 SQL 线程回放听起来简单,但问题来了: 备库怎么知道该从 binlog 的哪个位置开始拉取? 如果主库在写入 binlog 后、还没发给备库就宕机了,怎么办? ---## 二、关键机制:binlog 的"三阶段写入"和"同步复制"### 1. 主库的 binlog 何时写入?MySQL 默认是异步复制 ,意思是主库写完 binlog 后,并不等备库确认,就直接返回给客户端。这样性能高,但风险是:主库宕机时,备库可能少收到几条 binlog。为了解决这个问题,MySQL 提供了半同步复制(Semisync Replication) ,它要求主库在提交事务前,至少等待一个备库确认收到 binlog。如果备库没确认,主库会等待,直到超时后再降级为异步。代码示例(配置半同步复制,在 MySQL 中执行):sql-- 主库安装半同步插件INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';SET GLOBAL rpl_semi_sync_master_enabled = 1;SET GLOBAL rpl_semi_sync_master_timeout = 1000; -- 超时1秒-- 备库安装半同步插件INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';SET GLOBAL rpl_semi_sync_slave_enabled = 1;-- 重启备库 IO 线程STOP SLAVE IO_THREAD;START SLAVE IO_THREAD;半同步复制的意义 :主库提交事务时,必须等到备库把 binlog 写入 relay log 并返回 ACK,才向客户端返回成功。这样主库宕机时,至少有一个备库拥有最新数据。---### 2. 备库如何"回放" binlog?备库的 SQL 线程会读取 relay log,然后执行其中的 SQL。但注意:binlog 里存的不是原始 SQL,而是"事件" ,比如"插入一行记录"而不是"INSERT INTO ... WHERE ..."。这是因为 binlog 要支持主备切换后的数据一致性,必须记录"行级别"的变更。看一个实际例子,假设主库执行:sql-- 主库执行:把 id=1 的 age 加 1UPDATE user SET age = age + 1 WHERE id = 1;binlog 里记录的是类似:### UPDATE `user`### WHERE### @1=1 /* id */### SET### @3=25 /* age 从 24 改为 25 */备库回放时,直接按照这个"行变更"去更新,不需要重新执行 WHERE 条件,所以即使表结构有细微差异,也能保证最终一致。---## 三、主备切换的"惊险一跃":如何保证不丢数据?如果主库突然宕机,需要把备库提升为主库。这时,最怕的是:备库的 relay log 还没执行完,或者备库的 binlog 位置落后于主库。MySQL 8.0 引入了增强半同步复制(Lossless Semi-sync) ,它把"备库写入 relay log"和"主库提交事务"绑定在一起:主库在提交事务时,必须等备库把 binlog 写入 relay log 并落盘,才算成功。看一个配置示例(MySQL 8.0 中):sql-- 主库SET PERSIST rpl_semi_sync_master_enabled = ON;SET PERSIST rpl_semi_sync_master_wait_point = AFTER_SYNC; -- 默认值,等备库写入后主库才提交-- 备库SET PERSIST rpl_semi_sync_slave_enabled = ON;````AFTER_SYNC` 的含义是:主库把 binlog 发给备库后,**等待备库写盘,然后主库才提交事务**。这样即使主库宕机,备库也一定拥有这条 binlog,因此不会丢数据。---## 四、备库延迟怎么办?并行复制来救场主备同步最大的敌人是**延迟**,比如备库执行慢,导致主库已经写入 100 万条,备库才执行到 50 万条。在 MySQL 5.7 之前,备库是**单线程回放**,延迟容易积累。MySQL 5.7+ 提供了**并行复制(Multi-Threaded Replication)**,可以把 relay log 按数据库或表分组,多线程并行执行。配置示例:sql-- 备库设置并行复制线程数(假设 4 个线程)STOP SLAVE;SET GLOBAL slave_parallel_workers = 4;SET GLOBAL slave_parallel_type = LOGICAL_CLOCK; -- 基于逻辑时钟,自动判断事务之间的依赖START SLAVE;这样,备库可以同时执行多个不冲突的事务,大大降低延迟。---## 五、实战测试:模拟主库宕机,看备库是否丢数据下面用一个 Python 脚本模拟主库写入事务,同时监控备库数据:pythonimport pymysqlimport time# 连接主库和备库master_conn = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='123456')slave_conn = pymysql.connect(host='127.0.0.1', port=3307, user='root', password='123456')# 主库写入一条数据with master_conn.cursor() as cursor: cursor.execute("CREATE DATABASE IF NOT EXISTS test_db") cursor.execute("USE test_db") cursor.execute("CREATE TABLE IF NOT EXISTS t (id INT PRIMARY KEY, val VARCHAR(100))") cursor.execute("INSERT INTO t (id, val) VALUES (1, 'hello')")master_conn.commit()# 立即杀掉主库进程(模拟宕机)import osos.system("kill -9 <master_mysql_pid>")# 检查备库是否有这条数据(等待几秒让同步完成)time.sleep(2)with slave_conn.cursor() as cursor: cursor.execute("USE test_db") cursor.execute("SELECT * FROM t WHERE id=1") result = cursor.fetchone()if result: print("备库数据完整,值 =", result1)else: print("备库数据缺失!")```如果配置了半同步复制,这里的输出应该是"备库数据完整",因为主库在提交前已经等备库确认了。---## 六、总结MySQL 主备一致的核心是一套"日志接力"机制:1. binlog 记录所有变更 ,备库通过 IO 线程拉取,写入 relay log。2. 半同步复制 让主库提交事务前必须等备库确认,避免主库宕机丢数据。3. 并行复制 解决了备库延迟问题。4. 主备切换时,通过 binlog 位置和 relay log 的完整性,保证不丢数据。但要注意:没有绝对的一致,异步复制下,主备可能有秒级延迟;半同步下,如果超时降级,也可能丢数据。所以设计高可用系统时,还要结合心跳检测、自动切换等机制,才能做到"尽量不丢,快速恢复"。希望这篇文章能帮你更好地理解 MySQL 主备同步的底层逻辑。如果有疑问,欢迎在评论区讨论!