MySQL——保证主从一致

本文深入探讨MySQL主备切换流程及Binlog机制,包括主备数据同步、只读模式设置、Binlog三种格式对比、row格式数据恢复等关键概念。详细解释了如何在主备结构中实现数据一致性,以及Binlog在主备同步中的作用。

binlog 可以用来归档,也可以用来做主备同步,备库执行了 binlog 就可以跟主库保持一致。

MySQL 主备的基本原理

如图 1 所示就是基本的主备切换流程。

在状态 1 中,客户端的读写都直接访问节点 A,而节点 B 是 A 的备库,只是将 A 的更新都同步过来,到本地执行。这样可以保持节点 B 和 A 的数据是相同的。

当需要切换的时候,就切成状态 2。这时候客户端读写访问的都是节点 B,而节点 A 是 B 的备库。

在状态 1 中,虽然节点 B 没有被直接访问,但是我依然建议你把节点 B(也就是备库)设置成只读(readonly)模式。这样做,有以下几个考虑:

  1. 有时候一些运营类的查询语句会被放到备库上去查,设置为只读可以防止误操作;

  2. 防止切换逻辑有 bug,比如切换过程中出现双写,造成主备不一致;

  3. 可以用 readonly 状态,来判断节点的角色。

把备库设置成只读了,还怎么跟主库保持同步更新呢?readonly 设置对超级 (super) 权限用户是无效的,而用于同步更新的线程,就拥有超级权限。

接下来,我们再看看节点 A 到 B 这条线的内部流程是什么样的。图 2 中画出的就是一个 update 语句在节点 A 执行,然后同步到节点 B 的完整流程图。

 主库接收到客户端的更新请求后,执行内部事务的更新逻辑,同时写 binlog。

 备库 B 跟主库 A 之间维持了一个长连接。主库 A 内部有一个线程,专门用于服务备库 B 的这个长连接。一个事务日志同步的完整过程是这样的:

  1. 在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量

  2. 在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io_thread 和 sql_thread。其中 io_thread 负责与主库建立连接

  3. 主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog,发给 B

  4. 备库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)

  5. sql_thread 读取中转日志,解析出日志里的命令,并执行。

binlog 的三种格式对比

 binlog 有两种格式,一种是 statement,一种是 row。还有第三种叫做mixed,其实是前两种的混合。以以下的表作为例子:

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `a` int(11) DEFAULT NULL,
  `t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `a` (`a`),
  KEY `t_modified`(`t_modified`)
) ENGINE=InnoDB;
 
insert into t values(1,1,'2018-11-13');
insert into t values(2,2,'2018-11-12');
insert into t values(3,3,'2018-11-11');
insert into t values(4,4,'2018-11-10');
insert into t values(5,5,'2018-11-09');

如果要在表中删除一行数据的话,我们来看看这个 delete 语句的 binlog 是怎么记录的。

delete from t /*comment*/  where a>=4 and t_modified<='2018-11-10' limit 1;

当 binlog_format=statement 时,binlog 里面记录的就是 SQL 语句的原文:

 图 3 的输出结果:

  • 第二行是一个 BEGIN,跟第四行的 commit 对应,表示中间是一个事务;
  • 第三行就是真实执行的语句了。可以看到,在真实执行的 delete 命令之前,还有一个“use ‘test’”命令。这条命令不是我们主动执行的,而是 MySQL 根据当前要操作的表所在的数据库,自行添加的。这样做可以保证日志传到备库去执行的时候,不论当前的工作线程在哪个库里,都能够正确地更新到 test 库的表 t。
    use 'test’命令之后的 delete 语句,就是我们输入的 SQL 原文了。可以看到,binlog“忠实”地记录了 SQL 命令,甚至连注释也一并记录了。
  • 最后一行是一个 COMMIT。可以看到里面写着 xid=61。

 运行这条 delete 命令产生了一个 warning,原因是当前 binlog 设置的是 statement 格式,并且语句中有 limit,所以这个命令可能是 unsafe 的。

因为 delete 带 limit,很可能会出现主备数据不一致的情况。比如上面这个例子:

  1. 如果 delete 语句使用的是索引 a,那么会根据索引 a 找到第一个满足条件的行,也就是说删除的是 a=4 这一行;

  2. 但如果使用的是索引 t_modified,那么删除的就是 t_modified='2018-11-09’也就是 a=5 这一行。

由于 statement 格式下,记录到 binlog 里的是语句原文,因此可能会出现这样一种情况:在主库执行这条 SQL 语句的时候,用的是索引 a;而在备库执行这条 SQL 语句的时候,却使用了索引 t_modified。因此,MySQL 认为这样写是有风险的。

如果把 binlog 的格式改为 binlog_format=‘row’:

 

与 statement 格式的 binlog 相比,前后的 BEGIN 和 COMMIT 是一样的。但是,row 格式的 binlog 里没有了 SQL 语句的原文,而是替换成了两个 event:Table_map 和 Delete_rows。

  1. Table_map event,用于说明接下来要操作的表是 test 库的表 t;

  2. Delete_rows event,用于定义删除的行为。

 使用mysqlbinlog 工具解析和查看 binlog 中的内容:

 

可以看到以下几个信息:

  • server id 1,表示这个事务是在 server_id=1 的这个库上执行的。
  • 每个 event 都有 CRC32 的值,这是因为我把参数 binlog_checksum 设置成了 CRC32。
  • Table_map event 跟在图 5 中看到的相同,显示了接下来要打开的表,map 到数字 226。现在我们这条 SQL 语句只操作了一张表,如果要操作多张表呢?每个表都有一个对应的 Table_map event、都会 map 到一个单独的数字,用于区分对不同表的操作。
  • 我们在 mysqlbinlog 的命令中,使用了 -vv 参数是为了把内容都解析出来,所以从结果里面可以看到各个字段的值(比如,@1=4、 @2=4 这些值)。
  • binlog_row_image 的默认配置是 FULL,因此 Delete_event 里面,包含了删掉的行的所有字段的值。如果把 binlog_row_image 设置为 MINIMAL,则只会记录必要的信息,在这个例子里,就是只会记录 id=4 这个信息。
  • 最后的 Xid event,用于表示事务被正确地提交了。

 可以看到,当 binlog_format 使用 row 格式的时候,binlog 里面记录了真实删除行的主键 id,这样 binlog 传到备库去的时候,就肯定会删除 id=4 的行,不会有主备删除不同行的问题。

为什么会有 mixed 格式的 binlog?

为什么会有 mixed 这种 binlog 格式的存在场景?推论过程是这样的:

  • 因为有些 statement 格式的 binlog 可能会导致主备不一致,所以要使用 row 格式。
  • 但 row 格式的缺点是,很占空间。比如用一个 delete 语句删掉 10 万行数据,用 statement 的话就是一个 SQL 语句被记录到 binlog 中,占用几十个字节的空间。但如果用 row 格式的 binlog,就要把这 10 万条记录都写到 binlog 中。这样做,不仅会占用更大的空间,同时写 binlog 也要耗费 IO 资源,影响执行速度
  • 所以,MySQL 就取了个折中方案,也就是有了 mixed 格式的 binlog。mixed 格式的意思是,MySQL 自己会判断这条 SQL 语句是否可能引起主备不一致,如果有可能,就用 row 格式,否则就用 statement 格式。

也就是说,mixed 格式可以利用 statment 格式的优点,同时又避免了数据不一致的风险。

比如我们这个例子,设置为 mixed 后,就会记录为 row 格式;而如果执行的语句去掉 limit 1,就会记录为 statement 格式。 

 现在越来越多的场景要求把 MySQL 的 binlog 格式设置成 row。可以直接看出来的好处:恢复数据。分别从 delete、insert 和 update 这三种 SQL 语句的角度,来看看数据恢复的问题:

通过对row格式的解析和查看详细信息,可以看出来,即使我执行的是 delete 语句,row 格式的 binlog 也会把被删掉的行的整行信息保存起来。所以,如果你在执行完一条 delete 语句以后,发现删错数据了,可以直接把 binlog 中记录的 delete 语句转成 insert把被错删的数据插入回去就可以恢复了

如果你是执行错了 insert 语句呢?那就更直接了。row 格式下,insert 语句的 binlog 里会记录所有的字段信息,这些信息可以用来精确定位刚刚被插入的那一行。这时,你直接把 insert 语句转成 delete 语句,删除掉这被误插入的一行数据就可以了

如果执行的是 update 语句的话,binlog 里面会记录修改前整行的数据和修改后的整行数据。所以,如果你误执行了 update 语句的话,只需要把这个 event 前后的两行信息对调一下,再去数据库里面执行,就能恢复这个更新操作了。

循环复制问题

通过上面对 MySQL 中 binlog 基本内容的理解,你现在可以知道,binlog 的特性确保了在备库执行相同的 binlog,可以得到与主库相同的状态。

 因此,我们可以认为正常情况下主备的数据是一致的。也就是说,图 1 中 A、B 两个节点的内容是一致的。

对比上图和第一张图, 可以发现,双 M 结构和 M-S 结构,其实区别只是多了一条线,即:节点 A 和 B 之间总是互为主备关系。这样在切换的时候就不用再修改主备关系。实际生产上使用比较多的是双 M 结构

双 M 结构还有一个问题需要解决。

业务逻辑在节点 A 上更新了一条语句,然后再把生成的 binlog 发给节点 B,节点 B 执行完这条更新语句后也会生成 binlog。那么,如果节点 A 同时是节点 B 的备库,相当于又把节点 B 新生成的 binlog 拿过来执行了一次,然后节点 A 和 B 间,会不断地循环执行这个更新语句,也就是循环复制了

在上面对row格式的详细分析图中可以看到,MySQL 在 binlog 中记录了这个命令第一次执行时所在实例的 server id。因此,我们可以用下面的逻辑,来解决两个节点间的循环复制的问题:

  1. 规定两个库的 server id 必须不同,如果相同,则它们之间不能设定为主备关系;

  2. 一个备库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog;

  3. 每个库在收到从自己的主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志。

按照这个逻辑,如果我们设置了双 M 结构,日志的执行流就会变成这样:

  1. 从节点 A 更新的事务,binlog 里面记的都是 A 的 server id;

  2. 传到节点 B 执行一次以后,节点 B 生成的 binlog 的 server id 也是 A 的 server id;

  3. 再传回给节点 A,A 判断到这个 server id 与自己的相同,就不会再处理这个日志。所以,死循环在这里就断掉了。

<think>嗯,用户的问题很明确,就是想了解MySQL主从同步场景下如何保证数据一致性。从引用的材料来看,用户可能是个DBA或者后端开发,正在搭建或维护主从架构,遇到了数据延迟或不一致的问题。用户提供的引用内容相当全面,覆盖了从官方方案到第三方工具的各种解决方案。我需要把这些信息结构化,同时注意几个技术要点:首先用户提到“异步复制、半同步复制、组复制”这三种官方方案,这是个很好的框架;其次pt工具和缓存方案这些实战内容也很关键。考虑到用户的工作场景,我应该先强调最常用的半同步复制方案,因为它是平衡安全性和性能的折中选择。不过材料4提到半同步会降低吞吐量,这点必须明确告知用户。对于金融级场景,可以重点介绍组复制的多主模式,虽然引用里没展开说原理,但MGR的共识算法确实能解决脑裂问题。用户引用的pt-table-checksum工具方案非常实用,但要注意提醒风险点:比如表必须有主键这个硬性要求,还有pt-table-sync可能覆盖数据的问题。材料3里提到行级锁机制保证checksum准确性的细节很专业,可以保留在解决方案里。读写分离优化那块,用户引用2的配置示例其实暗藏玄机——GTID模式配合log_slave_updates能大幅降低延迟,这个可以揉到“配置优化”条目里。另外材料4提到的缓存标记法比较取巧,适合高并发但数据敏感性不强的场景,不过会增加系统复杂度。最后用户没问但应该考虑的是方案选型建议。我可以按一致性强度分级:先推半同步+MGR这种基础方案,再补充中间件/缓存这种需要改造的方案。对于紧急修复的场景,单独列出pt工具这类急救手段。回复结构上,准备按官方方案→第三方工具→配置/架构优化三层展开,最后给选型建议。要特别注意把所有技术术语用中文准确表述,比如binlog_row_image这种参数名保持大写但中文注释要清晰。用户给的示例引用很多,要把文献标记按引用顺序挂到具体方案上,比如半同步对应材料1和4。对了,用户之前的对话是系统指令,说明可能需要生成相关问题。可以围绕监控手段(如延迟检测)、容灾切换(主库宕机时保证一致)、具体工具实操(pt使用细节)来设计问题,这样能延伸用户的知识链。</think> 以下是在MySQL主从架构中保证数据一致性的核心解决方案总结,结合技术原理与实践经验分层说明: --- ### **一、MySQL官方方案** #### 1. **异步复制(默认模式)** - **原理**:主库提交事务后**立即返回客户端**,不等待从库确认(弱一致性)。 - **风险**:主库宕机可能导致从库丢失数据([^1][^4])。 - **适用场景**:可容忍少量数据丢失的读写分离场景。 #### 2. **半同步复制(Semi-Sync Replication)** - **原理**:主库提交事务后,需等待**至少一个从库写入Relay Log**才返回客户端(强一致性)。 - **配置**: ```ini # my.cnf plugin_load = "rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so" rpl_semi_sync_master_enabled = 1 rpl_semi_sync_slave_enabled = 1 ``` - **优点**:降低数据丢失风险。 - **缺点**:写入延迟增加,吞吐量下降([^1][^4])。 #### 3. **组复制(MySQL Group Replication, MGR)** - **原理**:基于Paxos协议实现多主同步,事务需**多数节点确认**才提交(最强一致性)。 - **模式**: - 单主模式(自动故障转移) - 多主模式(分布式写入) - **适用场景**:金融级高可用需求([^1])。 --- ### **二、第三方工具与架构优化** #### 1. **数据一致性校验工具** - **Percona Toolkit**(推荐): - **pt-table-checksum**:校验主从数据差异(需主键索引)。 ```bash pt-table-checksum --host=主库IP --user=root --password=密码 --databases=库名 ``` - **pt-table-sync**:修复不一致数据(在从库执行)。 ```bash pt-table-sync --execute --sync-to-master 从库IP --databases=库名 --user=root --password=密码 ``` - **注意事项**: - 校验时对数据块加`FOR UPDATE`锁保证一致性([^3])。 - 无主键表无法处理,需提前设计索引。 #### 2. **读写分离策略优化** - **读写分离中间件**(如MyCAT): - **强制读主库**:写操作后的读请求短暂路由到主库(例如500ms)。 - **动态路由**:记录写操作的key,同步窗口内对该key的读请求定向到主库([^4])。 - **配置示例**(增强同步可靠性): ```ini # my.cnf(GTID模式) binlog_format = ROW # 行级复制 binlog_row_image = FULL # 记录完整行数据 gtid_mode = ON # 开启GTID log_slave_updates = ON # 从库记录binlog(级联复制必备) ``` #### 3. **缓存标记写操作法** - **流程**: 1. 写请求:在缓存(如Redis)记录写操作的key,设置超时时间(≈主从延迟)。 2. 读请求:若缓存命中key,则读主库;否则读从库。 - **优点**:避免引入中间件,成本较低([^4])。 - **缺点**:依赖缓存可靠性,增加系统复杂度。 --- ### **三、关键配置与避坑指南** | **配置项** | **推荐值** | **作用** | |---------------------|----------------|-----------------------------| | `binlog_format` | `ROW` | 避免混合/语句复制导致数据歧义 | | `sync_binlog` | `1` | 每次提交同步binlog到磁盘 | | `innodb_flush_log_at_trx_commit` | `1` | 每次事务刷redo log到磁盘 | | `slave_parallel_workers` | >0 | 启用多线程复制降低延迟 | --- ### **四、方案选型建议** 1. **强一致性场景**: - 优先选择 **MGR** 或 **半同步复制+读写分离中间件**。 2. **允许短暂不一致**: - **异步复制+定期校验修复**(pt工具),配合缓存标记法。 3. **高频写场景**: - 优化配置(GTID+多线程复制)降低延迟风险([^2])。 > ⚠️ **注意事项** > - 所有方案需配合监控(如主从延迟`Seconds_Behind_Master`)。 > - 主从切换后必须校验数据一致性([^3][^4])。 --- ### **相关问题延伸** 1. **如何实时监控MySQL主从延迟?** - 监控指标:`SHOW SLAVE STATUS`中的`Seconds_Behind_Master`、`Relay_Log_Pos`。 - 工具推荐:Prometheus + Grafana(集成MySQL Exporter)。 2. **主库宕机时如何快速恢复一致性?** - 步骤: ① 立即停止所有写入; ② 使用`pt-table-checksum`定位差异; ③ 通过`pt-table-sync`修复或重建从库([^3])。 3. **为什么GTID模式能提升一致性?** - 原理:全局事务ID唯一标识每个事务,避免传统复制中位点(binlog file+pos)不一致导致的复制中断([^2][^4])。 4. **无主键表如何校验数据一致性?** - 临时方案:添加自增主键后再校验。 - 终极方案:重构表结构(强制要求主键)([^3])。 [^1]: 主从同步基础方案 [^2]: 延迟优化配置 [^3]: 一致性校验与修复 [^4]: 读写分离与缓存策略
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值