MySQL数据一致性为什么总出问题高并发主从同步场景下的数据错乱如何排查与修复
MySQL主从同步出问题这件事,在业界已经不是什么新鲜事了。我见过太多团队被这个问题折磨得痛不欲生,特别是高并发场景下,数据在主库和从库之间”各跑各的”,最后对账对到怀疑人生。今天咱们就坐下来,像聊天一样把这个话题掰开揉碎聊聊。
先说说,主从同步到底在干嘛
想象一下,你开了一家连锁便利店。总店(主库)负责进货、收款、记账,分店(从库)负责卖货、接待顾客。总店每做一笔生意,都会把记录发给分店,分店照着记下来。这个”发记录”的过程,就是MySQL的主从同步。
具体来说,主库把所有修改数据的操作(INSERT、UPDATE、DELETE)记录到二进制日志(binlog)里,然后从库通过一个IO线程把binlog拉到本地,再有一个SQL线程回放执行。听起来很简单对吧?但在高并发场景下,这个”看起来简单”的过程会涌现出各种问题。
为什么高并发会让主从同步出问题
时钟抖动和复制延迟
这是最常见的问题。主库和从库各自有系统时钟,虽然NTP会同步,但网络抖动、磁盘IO、CPU调度等因素,会让从库的回放速度跟不上主库的写入速度。
-- 查看主从同步状态
SHOW SLAVE STATUS\G
-- 关键字段解读
-- Seconds_Behind_Master: 从库落后主库多少秒
-- Master_Log_File: 从库正在读取的主库binlog文件
-- Relay_Log_File: 从库的中继日志文件
-- Last_SQL_Error: 最后一条SQL执行的错误信息
-- Relay_Log_Space: 中继日志的总大小
想象一下,主库一秒钟写入1000条数据,从库一秒钟只能回放200条。这一来二去,从库就落后了5秒、10秒、甚至几分钟。在这几秒到几分钟的时间窗口里,如果你读取从库,读到的可能是”过时”的数据。
半同步复制的”半吊子”问题
MySQL提供了半同步复制(semi-sync)来缓解这个问题。配置方式是:
-- 主库安装半同步插件
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
-- 从库安装半同步插件
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
-- 主库配置
SET GLOBAL rpl_semi_sync_master_enabled = ON;
SET GLOBAL rpl_semi_sync_master_timeout = 1000; -- 1秒超时
-- 从库配置
SET GLOBAL rpl_semi_sync_slave_enabled = ON;
半同步的意思是:主库写数据后,不会立刻返回给客户端,而是等待至少一个从库确认收到binlog事件后再返回。这样能确保数据不会丢失,但是……从库可能还没有回放完啊!
这就好比你说”快递已经出库了”,但实际上快递还在路上。如果你此时读取从库,读到的可能是空数据。
跨行事务的复制问题
这个坑比较深。假设你有一个业务逻辑:
-- 场景:用户下单,扣减库存
START TRANSACTION;
UPDATE inventory SET stock = stock - 1 WHERE product_id = 1001;
INSERT INTO orders (product_id, user_id, quantity) VALUES (1001, 12345, 1);
COMMIT;
在主库上,这个事务是原子的,要么一起成功,要么一起失败。但是在从库上,如果两个表在不同的物理服务器上,复制时可能出现问题:
-- 如果inventory表和orders表分库分表了
-- 从库A回放inventory表的UPDATE
-- 从库B回放orders表的INSERT
-- 此时从库A还没有orders的数据,从库B还没有inventory的变更
这就导致了数据不一致。
GTID的坑
GTID(Global Transaction ID)是MySQL 5.6引入的特性,目的是让主从复制更可靠。每个事务都有一个全局唯一的ID,从库通过GTID来判断自己是否已经执行过某个事务。
-- 查看GTID信息
SHOW VARIABLES LIKE 'gtid_mode';
SHOW MASTER STATUS;
SHOW BINARY LOGS;
-- 查看已执行的事务
SELECT * FROM mysql.gtid_executed;
但是GTID也不是万能的。如果主库和从库的binlog格式不一致,或者复制拓扑结构复杂(比如双主、环形复制),GTID可能会出问题。
数据错乱的典型场景
场景一:从库读到旧数据
这是最常见的场景。业务逻辑是这样的:
用户下单 → 写主库 → 返回成功 → 读从库 → 展示订单
由于主从复制延迟,用户在写主库后立即读从库,可能读不到自己刚写的订单。这在电商、金融场景下是致命的问题。
排查方法:
-- 在主库和从库分别查询
-- 主库
SELECT * FROM orders WHERE order_id = 'xxx';
-- 从库(等待同步完成后)
SELECT * FROM orders WHERE order_id = 'xxx';
-- 对比结果,如果从库没有数据,说明有延迟
解决思路:对于强一致性的业务,不要读从库,直接读主库。或者使用MySQL的”读主库”特性:
-- MySQL 8.0+ 支持在查询前加一个hint,强制读主库
SELECT * FROM orders WHERE order_id = 'xxx' /* READ_MASTER */;
-- 或者使用MySQL的GROUP_READ_WRITE模式
SET SESSION TRANSACTION READ WRITE;
SELECT * FROM orders WHERE order_id = 'xxx';
场景二:数据被覆盖
这个场景比较隐蔽。假设你的表结构是这样的:
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
version INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
在高并发场景下,两个事务同时更新同一行:
事务A: UPDATE users SET name = 'Alice', version = version + 1 WHERE id = 1;
事务B: UPDATE users SET name = 'Bob', version = version + 1 WHERE id = 1;
如果事务A先提交,事务B后提交,主库上的结果是name=‘Bob’, version=2。但是从库的复制顺序可能不同,导致结果是name=‘Alice’, version=2。
排查方法:
-- 开启binlog row格式的模糊查询
SELECT * FROM mysql.binlog WHERE Log_name = 'mysql-bin.000001';
-- 或者使用mysqlbinlog工具解析
mysqlbinlog --start-datetime='2024-01-01 00:00:00' \
--stop-datetime='2024-01-01 01:00:00' \
/var/log/mysql/mysql-bin.000001 | grep -A5 'UPDATE users';
解决思路:使用乐观锁或者 pessimistic lock,确保并发更新不会冲突:
-- 乐观锁方式
UPDATE users SET name = 'Bob', version = version + 1
WHERE id = 1 AND version = 1;
-- 如果受影响的行数为0,说明有冲突,需要重试
-- 悲观锁方式
START TRANSACTION;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
UPDATE users SET name = 'Bob' WHERE id = 1;
COMMIT;
场景三:DDL和DML混合执行
这是一个很容易被忽视的问题。假设你在主库上执行了DDL语句:
ALTER TABLE orders ADD COLUMN status VARCHAR(20);
然后从库在回放这个DDL时,可能因为锁表或者其他原因延迟执行。而此时,主库上已经有新的DML操作在修改这张表。从库回放DDL后,可能会丢失这段时间的DML操作。
排查方法:
-- 查看主库的binlog位置
SHOW MASTER STATUS;
-- 查看从库的复制位置
SHOW SLAVE STATUS;
-- 对比两者的Relay_Log_Space和Read_Master_Log_Pos
解决思路:避免在生产高峰期执行DDL,或者使用pt-online-schema-change等工具在线修改表结构。
排查数据错乱的工具和方法
工具一:pt-table-checksum
这是Percona Toolkit提供的数据校验工具,可以对比主库和从库的数据一致性。
# 基本用法
pt-table-checksum \
--host=master_host \
--user=admin \
--password=secret \
--databases=your_database \
--tables=your_table
# 输出示例
TS CHUNS QUANTUM COUNT DIFF CRC
15:30:00 1 1 100 0 0
DIFF列表示主库和从库的差异行数,如果DIFF不为0,说明数据不一致。
工具二:pt-table-sync
发现不一致后,可以用pt-table-sync来修复。
# 修复数据不一致
pt-table-sync \
--execute \
--print \
h=master_host,u=admin,p=secret \
h=slave_host,u=admin,p=secret \
--databases=your_database \
--tables=your_table
这个工具会生成修复SQL,你可以先查看(不带–execute),确认无误后再执行。
工具三:Binlog分析
对于复杂的数据错乱问题,最直接的方法是分析binlog。
# 解析binlog
mysqlbinlog --start-position=12345 --stop-position=67890 \
/var/log/mysql/mysql-bin.000001 > /tmp/binlog_analysis.sql
# 查看具体的SQL操作
cat /tmp/binlog_analysis.sql | grep -E '(INSERT|UPDATE|DELETE)'
工具四:监控和告警
建立一个完善的监控体系,可以在问题发生前发现异常。
-- 监控主从延迟
SELECT
MAX(Seconds_Behind_Master) AS max_delay,
COUNT(*) AS slave_count
FROM information_schema.processlist
WHERE COMMAND = 'Sleep';
-- 监控复制错误
SELECT
Channel_Name,
Last_Error,
Last_SQL_Error_Timestamp
FROM replication_slave_status;
修复数据错乱的正确姿势
第一步:止血
发现数据不一致后,首先要停止从库的复制,防止问题扩大。
STOP SLAVE;
第二步:定位问题
使用前面提到的工具,定位是哪张表、哪个时间段出现了问题。
第三步:评估影响
确定数据不一致的影响范围,是需要全部修复,还是可以接受部分不一致。
第四步:修复数据
根据问题的严重程度,选择不同的修复策略。
策略一:从binlog重建
如果从库的数据丢失严重,可以直接从主库的binlog重建。
# 从主库导出binlog
mysqlbinlog --start-datetime='2024-01-01 00:00:00' \
--stop-datetime='2024-01-01 23:59:59' \
/var/log/mysql/mysql-bin.000001 > /tmp/full_binlog.sql
# 在从库上重新执行
mysql -u root -p your_database < /tmp/full_binlog.sql
策略二:使用pt-table-sync修复
对于小范围的数据不一致,使用pt-table-sync更简单。
策略三:全量同步
如果问题太严重,可以直接停止从库服务,从主库全量复制一份数据。
# 在主库上导出全量数据
mysqldump --single-transaction --routines --triggers \
-u root -p your_database > full_backup.sql
# 在从库上恢复
mysql -u root -p your_database < full_backup.sql
第五步:恢复复制
数据修复完成后,重新启动主从复制。
-- 查看主库的binlog位置
SHOW MASTER STATUS;
-- 在从库上设置复制位置
CHANGE MASTER TO
MASTER_HOST='master_host',
MASTER_USER='repl_user',
MASTER_PASSWORD='repl_password',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=12345;
-- 启动复制
START SLAVE;
-- 检查复制状态
SHOW SLAVE STATUS\G
预防胜于治疗
排查和修复数据错乱是很痛苦的过程,最好的办法是预防。
1. 选择合适的复制模式
对于强一致性要求的场景,使用半同步复制或者MGR(MySQL Group Replication)。
-- MGR配置示例
mysqlsh -- uri root@localhost
\connect mysql://root@localhost:3306
\sql
SET GLOBAL group_replication_bootstrap_group=ON;
CALL group_replication_setup_instances('user@host:port');
SET GLOBAL group_replication_bootstrap_group=OFF;
CALL group_replication_start();
2. 监控复制状态
建立监控告警,及时发现复制延迟和错误。
-- 监控复制延迟的SQL
SELECT
r.Channel_Name,
r.Slave_IO_Running,
r.Slave_SQL_Running,
r.Seconds_Behind_Master,
r.Last_IO_Error,
r.Last_SQL_Error
FROM information_schema.replication_slave_status r;
3. 避免在从库上执行写入操作
这是老生常谈,但很重要。从库只用于读取,避免任何写入操作,可以大大减少数据不一致的风险。
4. 定期做数据校验
使用pt-table-checksum等工具,定期对比主库和从库的数据一致性。
# 每天定时执行数据校验
0 3 * * * /usr/bin/pt-table-checksum \
--host=master_host \
--user=admin \
--password=secret \
--databases=your_database \
--quiet
总结一下
MySQL主从同步的数据一致性问题,本质上是一个分布式系统的一致性问题。在网络分区、时钟漂移、复制延迟等因素的影响下,保持数据强一致是很困难的。
我的建议是:
- 理解问题:了解主从同步的原理和可能的问题点,才能在遇到问题时快速定位。
- 建立监控:没有监控就没有话语权,建立完善的主从监控体系。
- 定期校验:不要让数据不一致积累到无法收拾的地步。
- 做好预案:数据出问题了怎么办?修复流程是什么?提前想好。
记住,数据一致性是一个持续的过程,不是一劳永逸的事情。希望这篇文章能帮到你。
