MySQL 备份与运维

📌 本文由 11 篇相关文章智能合并整理而成

基于时间点恢复 PITR 实战

基于时间点恢复 PITR 实战

什么是 PITR

PITR(Point-In-Time Recovery,时间点恢复)是指将数据库恢复到某个特定的时间点,而不是恢复到最近的备份时刻。

例如:下午 3:00 不小心 DROP TABLE users;,PITR 可以将数据恢复到 2:59:59 的状态,从而把”丢失的表”找回来。

PITR 的核心要素

完整备份(全量)         +          Binlog 归档
    ↓                                 ↓
恢复备份到某个时间点       →   重放 Binlog 到目标时间点

PITR 的前提条件

1. 开启 Binlog

[mysqld]
log_bin = mysql-bin              # 开启 Binlog
binlog_format = ROW              # 推荐 ROW 格式(PITR 更精确)
binlog_row_image = FULL          # 记录完整的行镜像
expire_logs_days = 7             # 保留 7 天(调整到满足最长恢复需求)
# MySQL 8.0+
binlog_expire_logs_seconds = 604800  # 7 天

2. 保留 Binlog 文件

Binlog 文件是 PITR 的基础,必须妥善保存:

# 查看当前 Binlog 文件
mysql -e "SHOW BINARY LOGS;"

# 刷出新的 Binlog(通常在做全量备份前执行)
mysql -e "FLUSH LOGS;"

# 将 Binlog 定期归档到安全位置
# 如:同步到对象存储、远程备份服务器

3. 定期的全量备份

PITR 依赖于全量备份作为基础点。没有全量备份,光有 Binlog 是无法恢复的(总不能让 MySQL 从 binlog.000001 一直重放到现在)。

# 建议全量备份周期
# 大型数据库:每周 1 次全量 + 每天增量
# 中型数据库:每天全量
# 重要业务:每天全量 + 每 6 小时增量

PITR 完整步骤

场景:恢复数据库到 2025-06-15 14:59:59

第一步:准备全量备份

# 找到最近一次正常的全量备份
ls -la /backup/mysql/full/

# 使用 xtrabackup 恢复
xtrabackup --prepare --target-dir=/backup/mysql/full/20250614/
xtrabackup --copy-back --target-dir=/backup/mysql/full/20250614/
chown -R mysql:mysql /var/lib/mysql/

# 或者使用 mysqldump 备份的恢复
mysql -u root -p < /backup/mysql/mydb_20250614.sql

第二步:找出目标 Binlog 位置

# 方法一:使用 mysqlbinlog 查看
mysqlbinlog --start-datetime="2025-06-15 14:59:59" \
            --stop-datetime="2025-06-15 15:00:01" \
            /var/lib/mysql/mysql-bin.000123 | grep -i "DROP TABLE"

# 方法二:查看备份记录的 Binlog 位置
# xtrabackup 备份目录中的 xtrabackup_binlog_info 文件记录了位置
cat /backup/mysql/full/20250614/xtrabackup_binlog_info
# 输出示例:mysql-bin.000120 438945618

第三步:应用 Binlog 到目标时间点

# 从全量备份的位置开始,重放到目标时间点
mysqlbinlog \
  --start-position=438945618 \
  --stop-datetime="2025-06-15 14:59:59" \
  /var/lib/mysql/mysql-bin.000120 \
  /var/lib/mysql/mysql-bin.000121 \
  /var/lib/mysql/mysql-bin.000122 \
  /var/lib/mysql/mysql-bin.000123 \
  | mysql -u root -p

简化版:使用 mysqlbinlog 的连续文件处理

# 将所有需要的 Binlog 合并为一个 SQL 文件
mysqlbinlog \
  --start-datetime="2025-06-14 03:00:00" \
  --stop-datetime="2025-06-15 14:59:59" \
  /var/lib/mysql/mysql-bin.00012* \
  > /tmp/pitr_recovery.sql

# 检查生成的 SQL 中是否有敏感操作(如 DROP)
grep -i "DROP TABLE" /tmp/pitr_recovery.sql

# 确认无误后执行恢复
mysql -u root -p < /tmp/pitr_recovery.sql

常见 PITR 场景

场景 1:误删除一张表

# 步骤
# 1. 恢复全量备份到临时实例
# 2. 从 Binlog 中提取该表的操作
# 3. 仅导出该表数据
# 4. 恢复到主库

# 提取特定表的 Binlog
mysqlbinlog \
  --database=mydb \
  --start-datetime="2025-06-14 03:00:00" \
  --stop-datetime="2025-06-15 14:59:59" \
  /var/lib/mysql/mysql-bin.00012* | \
  grep -A 1000 "DELETE FROM users" | head -1000 > drop_recovery.sql

场景 2:全库回滚到某时间点

# 1. 准备新实例(防止影响生产环境)
docker run -d --name mysql_recovery -e MYSQL_ROOT_PASSWORD=xxx mysql:8.0

# 2. 恢复全量备份到新实例
# 3. 重放 Binlog
# 4. 验证数据正确性
# 5. 切换业务到新实例

场景 3:跳过误操作语句

# 精确跳过某个 DELETE 语句
# 找到 DELETE 的 Binlog 位置
mysqlbinlog mysql-bin.000123 | grep -n "DELETE FROM users"

# 分两段重放:在 DELETE 前停止,然后跳过 DELETE 继续
mysqlbinlog --stop-position=12345678 binlog.000123 | mysql -u root -p
mysqlbinlog --start-position=12345999 binlog.000123 | mysql -u root -p

PITR 的性能考量

恢复时间估算

恢复时间 ≈ 全量恢复时间 + Binlog 重放时间

Binlog 重放时间 ≈ Binlog 大小 / 重放速度
重放速度通常为:原写入速度的 50-70%(因为要执行 INSERT/UPDATE)

示例:
全量备份恢复:30 分钟(数据库 100GB)
Binlog 重放(6 小时的数据):1-2 小时
总计:1.5-2.5 小时

加速 PITR 的技巧

  1. 增大重放缓冲区--innodb_buffer_pool_size 调大
  2. 关闭 Binlog 写入:恢复时临时关闭 Binlog(--disable-log-bin
  3. 使用并行重放:将 Binlog 分段并行应用
  4. 增加 IO 能力:恢复时可使用更快的磁盘

PITR 的限制

限制 说明
Binlog 保留时间 只能恢复到最近一次 Binlog 被覆盖之前
大事务影响 大事务产生大量 Binlog,可能导致恢复延迟
RBR vs SBR Row 格式恢复慢但精确,Statement 格式恢复快但可能不一致
时间精度 精确到 Binlog 中记录的时间(微秒级)

最佳实践

  1. 每天做全量备份
  2. Binlog 至少保留 7 天
  3. 定期做 PITR 演练:每月至少一次模拟恢复
  4. Binlog 远程备份:防止服务器故障时 Binlog 也丢了
  5. 备份验证:在备份后验证备份文件可用
  6. 恢复到临时实例:先恢复到非生产环境验证,再切换到生产

面试常问题

Q:PITR 必须开启 Binlog 吗?
A:是的。Binlog 记录了所有数据变更,是 PITR 的基础。没有 Binlog 就只能恢复到备份时刻的状态,无法恢复到任意时间点。

Q:PITR 能恢复到误操作之前的时间点吗?
A:可以。关键是找到误操作的准确时间,在重放 Binlog 时用 --stop-datetime 在误操作之前停止。前提是 Binlog 记录的时间足够精确。

Q:恢复过程中如何避免把误操作也恢复进去?
A:在 Binlog 中找到误操作的准确位置,分段应用。先恢复到误操作之前,跳过误操作的语句,再继续应用后续的 Binlog。

Q:PITR 恢复需要多长时间?
A:取决于数据库大小和需要重放的 Binlog 量。一般经验是:恢复 1 小时的 Binlog 大约需要 30 分钟。全量越大恢复越慢,Binlog 越长恢复越久。


mysqldump 使用和注意事项

mysqldump 使用和注意事项

基本用法

mysqldump 是 MySQL 官方提供的逻辑备份工具,适合中小型数据库的备份和迁移。

基础语法

# 备份整个实例
mysqldump -u root -p --all-databases > full_backup.sql

# 备份单个数据库
mysqldump -u root -p mydb > mydb_backup.sql

# 备份单张表
mysqldump -u root -p mydb users > users_backup.sql

# 备份多个数据库
mysqldump -u root -p --databases db1 db2 db3 > multi_db.sql

常用选项详解

# 结构与数据分离
--no-data         # 只导出表结构,不导出数据
--no-create-info  # 只导出数据,不导出表结构

# 一致性与锁定
--single-transaction   # InnoDB 一致性快照(不锁表)
--lock-tables          # MyISAM 使用(锁所有表)
--lock-all-tables      # 全局锁所有数据库的表

# 内容控制
--where "id > 1000"     # 按条件导出数据
--ignore-table mydb.logs # 忽略指定表

# 对象导出
--routines    # 导出存储过程和函数
--triggers    # 导出触发器
--events      # 导出事件

# 压缩
mysqldump -u root -p mydb | gzip > mydb.sql.gz

# 指定字符集
--default-character-set=utf8mb4

生产环境备份示例

InnoDB 数据库备份(推荐)

mysqldump -u root -p \
  --single-transaction \
  --routines \
  --triggers \
  --events \
  --quick \
  --flush-logs \
  --master-data=2 \
  --set-gtid-purged=OFF \
  --default-character-set=utf8mb4 \
  mydb | gzip > /backup/mydb_$(date +%Y%m%d).sql.gz

参数说明:
--single-transaction:在 InnoDB 中使用事务获取一致性快照,不锁表(核心参数)
--quick:逐行读取,不缓冲到内存(减少内存消耗)
--flush-logs:刷新 Binlog,便于后续增量恢复
--master-data=2:记录 Binlog 位置(注释形式),用于搭建从库或 PITR
--set-gtid-purged=OFF:不记录 GTID 信息,或视需求改为 ON
--routines/--triggers/--events:导出数据库对象

MyISAM 数据库备份

mysqldump -u root -p \
  --lock-all-tables \
  --routines \
  --triggers \
  mydb > mydb_backup.sql

重要注意事项

1. –single-transaction 详解

这是 mysqldump 中最重要也最容易被误用的参数:

# 正确使用(InnoDB)
mysqldump --single-transaction --all-databases > backup.sql

# 错误使用(不能同时用 --single-transaction 和 --lock-tables)
mysqldump --single-transaction --lock-tables ...  # 冲突!

原理
--single-transaction 通过 START TRANSACTION WITH CONSISTENT SNAPSHOT 创建一个快照
– 整个备份过程在这个快照上进行,不影响其他会话的读写
– 但不适用于 MyISAM 表(MyISAM 不支持事务)

2. 备份过程中不要跑 DDL

即使使用了 --single-transaction,如果备份过程中有 ALTER TABLEDROP TABLETRUNCATE 等 DDL 操作,可能导致:
– 备份数据不一致
– 备份失败报错
– 备份进程被阻塞

3. 大表备份问题

对于大表(>10GB),mysqldump 可能遇到的问题:

# 备份超大表时可能报错
# max_allowed_packet 限制单条 SQL 大小

# 查看当前限制
mysql -e "SHOW VARIABLES LIKE 'max_allowed_packet'"

# 备份时增加包大小限制
mysqldump --max_allowed_packet=1G mydb > backup.sql

4. 内存消耗

# --quick 参数很重要
# 不指定 --quick 时,mysqldump 会将整个结果集加载到内存,大表可能 OOM

# 指定 --quick 后逐行读取,内存占用小
mysqldump --quick --single-transaction mydb > backup.sql

5. 恢复时的注意事项

# 基本恢复
mysql -u root -p mydb < backup.sql

# 恢复大文件时要分段执行(使用 pv 查看进度)
pv backup.sql | mysql -u root -p mydb

# 禁用外键检查加速导入
mysql -u root -p mydb -e "SET FOREIGN_KEY_CHECKS=0; SOURCE backup.sql; SET FOREIGN_KEY_CHECKS=1;"

# 禁用自动提交(加快批量插入)
mysql -u root -p mydb -e "SET autocommit=0; SOURCE backup.sql; COMMIT;"

6. 备份文件校验

# 备份时就校验
mysqldump --single-transaction --disable-keys mydb > backup.sql

# 恢复后检查表
mysqlcheck -u root -p --check mydb

# 使用 pt-table-checksum 校验(推荐)
pt-table-checksum h=localhost,u=root,p=password,d=mydb

mysqldump 的局限

局限 说明 替代方案
单线程 导出的全过程单线程 mysqlpump、mydumper 支持并行
大表慢 100GB+ 可能需几个小时 xtrabackup 物理备份
不支持增量 每次都是全量备份 xtrabackup 增量备份
不压缩 需要自己管道 gzip xtrabackup –compress

面试常问题

Q:mysqldump 备份时加 –single-transaction 还需要加 –lock-tables 吗?
A:不需要。–single-transaction 通过事务快照保证一致性,–lock-tables 是早期用于 MyISAM 的锁表方式,两者不兼容。

Q:mysqldump 备份出来的 SQL 文件,为什么有些 INSERT 特别长?
A:mysqldump 默认用扩展 INSERT(一条 INSERT 插多行)来提高效率和缩小文件大小。可以通过 --skip-extended-insert 改为每行一个 INSERT,但文件会变大。

Q:mysqldump 备份时会影响线上业务吗?
A:--single-transaction 下对 InnoDB 基本无影响。但:会占用 CPU 和 IO 资源;会生成大的读视图增加 MVCC 开销;备份 MyISAM 表必须锁表。建议在业务低峰期执行。

Q:数据库 100GB 能用 mysqldump 备份吗?
A:技术上可以,但恢复时间可能很长(几小时)。物理备份(xtrabackup)更适合这个量级。如果必须用 mysqldump,建议加上 --where 分批导出或使用 --quick 减少内存消耗,同时管道压缩以节省磁盘空间。


清理 Undo Log 占用的空间

清理 Undo Log 占用的空间

什么是 Undo Log

Undo Log 是 InnoDB 实现 MVCC(多版本并发控制)和事务回滚的核心组件。每条记录的修改操作都会生成一条 Undo Log,记录”修改前的数据是什么”。

-- 事务 A 执行
UPDATE users SET name = 'new_name' WHERE id = 1;
-- Undo Log 记录:id=1 的 name 原来是 'old_name'
-- 如果事务回滚,可以通过 Undo Log 恢复
-- 如果其他事务并发读取,通过 Undo Log 构建旧版本数据

Undo Log 的生命周期

事务开始 → 生成 Undo Log → 写入 Undo 表空间
事务提交 → Undo Log 不再被任何事务需要
         → 等待 Purge 线程清理
Purge 完成 → Undo Log 空间被回收

关键点:事务提交后,Undo Log 不会立即删除。因为可能还有更早启动的事务需要读取这些旧版本数据(MVCC Read View)。

Undo Log 在 MySQL 不同版本中的存储

MySQL 5.5 及之前

Undo Log 存储在共享表空间(ibdata1)中。
问题:ibdata1 持续膨胀,且无法缩小
TRUNCATEPURGE 都不会释放 ibdata1 空间

MySQL 5.6 / 5.7

引入了独立 Undo 表空间:

# 开启独立 Undo 表空间
innodb_undo_tablespaces = 2   # 使用 2 个 Undo 表空间文件(最大值 126)

# 默认位置
/var/lib/mysql/undo_001
/var/lib/mysql/undo_002

优点:Undo 表空间可以 TRUNCATE,不会导致 ibdata1 无限膨胀。

MySQL 8.0

MySQL 8.0 的 Undo 表空间得到进一步改进:

innodb_undo_tablespaces = 2        # 默认 2 个
innodb_max_undo_log_size = 1G     # 每个 Undo 表空间最大 1GB(默认)
innodb_purge_rseg_truncate_frequency = 128  # Purge 检查 TRUNCATE 的频率

关键改进:
1. 自动 TRUNCATEinnodb_undo_log_truncate = ON(8.0 默认开启)
2. undo tablespace 文件可以自动收缩
3. 更多 Undo 表空间:最多 127 个

Undo 表空间膨胀的原因

1. 大事务

-- 一个事务修改了 1000 万行
BEGIN;
UPDATE millions_of_rows SET status = 1;
-- 产生大量 Undo Log
COMMIT;
-- 即使提交了,Undo 也不会立即清理

2. 长事务

-- 事务一直不提交
BEGIN;
UPDATE users SET name = 'test' WHERE id = 1;
-- 事务保持 1 小时不提交
-- 期间生成的所有 Undo Log 都不能清理

3. 大查询 + 小事务混合

-- 会话 A:长时间运行的查询
SELECT * FROM users WHERE ...;  -- 可能扫描 30 分钟

-- 会话 B:同时更新表
UPDATE users SET ...;           -- 产生的 Undo 在 A 查询结束前不能释放

4. 活跃的 REPEATABLE READ 事务

-- 在 REPEATABLE READ 级别下
-- 事务开始后第一个 SELECT 创建 Read View
-- 之后的所有旧版本数据都必须保留到事务结束
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM ... ;  -- 创建 Read View
-- 后续对该表的更新产生的 Undo 都不能清理

监控 Undo 表空间

-- 查看 Undo 表空间大小(8.0)
SELECT TABLESPACE_NAME, FILE_NAME, 
       ROUND(SUM(FILE_SIZE) / 1024 / 1024, 2) AS size_mb
FROM information_schema.INNODB_TABLESPACES
WHERE TABLESPACE_NAME LIKE '%undo%';

-- 查看历史列表长度(pending purge 的记录数)
SHOW GLOBAL STATUS LIKE 'Innodb_history_list_length';
-- 这个值越大,说明需要 PURGE 的 Undo 越多

-- 查看当前活跃事务
SELECT * FROM information_schema.INNODB_TRX\G

Undo 表空间清理

MySQL 8.0 自动清理

默认自动开启 innodb_undo_log_truncate = ON

-- 确认配置
SHOW VARIABLES LIKE 'innodb_undo_log_truncate';  -- ON
SHOW VARIABLES LIKE 'innodb_max_undo_log_size';  -- 1048576000 (1GB)

当 Undo 表空间超过 innodb_max_undo_log_size 时,InnoDB 会自动进行 TRUNCATE 操作:将未使用的 Undo 表空间标记为 inactive,然后截断其文件大小,再重新激活。

MySQL 5.7 手动清理

-- 1. 查看 Undo 表空间状态
SELECT * FROM information_schema.INNODB_UNDO_TABLESPACES;

-- 2. 停用并截断(如果支持)
-- 5.7 中需确保 purge 线程正常工作且无长事务阻塞

通用优化手段

1. 查找并处理长事务

-- 找出运行时间最长的事务
SELECT trx_id, trx_started, 
       TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS seconds_running,
       trx_mysql_thread_id
FROM information_schema.INNODB_TRX
ORDER BY trx_started ASC;

-- 如果发现需要杀掉长时间运行的事务
KILL [trx_mysql_thread_id];

2. 减少大事务

-- 不要这样(一个事务修改太多行)
BEGIN;
DELETE FROM logs WHERE created_at < '2023-01-01';  -- 一次删 1 亿行?
COMMIT;

-- 改为分批
BEGIN;
DELETE FROM logs WHERE created_at < '2023-01-01' LIMIT 10000;
COMMIT;
-- 重复直到完成

3. 检查 Purge 线程

-- 查看 Purge 线程是否在正常推进
SHOW ENGINE INNODB STATUS\G

在输出中查看:

TRANSACTIONS
------------
Trx id counter 1567890
Purge done for trx's n:o < 1567800
undo n:o < 0 state: running
History list length 456  -- 这个值越小越好

History list length 的值:
- < 1000:正常
- 1000 - 10000:需要注意
- > 100000:严重堆积

4. 增加 Undo 表空间数量

# 更多的 Undo 表空间 = 更多的空间可以并行 TRUNCATE
innodb_undo_tablespaces = 4

Undo 表空间恢复

如果在 innodb_undo_directory 中有损坏的 Undo 表空间,MySQL 可能无法启动

解决方案:

# 在 my.cnf 中添加,跳过 Undo 验证
innodb_undo_log_truncate = OFF

# 或使用
innodb_force_recovery = 1

面试常问题

Q:事务提交了为什么 Undo Log 还在?
A:因为 MVCC 需要。如果有一个更早启动的事务还在读取数据,它需要 Undo Log 来构建旧版本。只有所有需要旧版本的事务都结束了,PURGE 线程才能清理。

Q:MySQL 8.0 的 Undo 表空间会自动缩小吗?
A:会。innodb_undo_log_truncate = ON(默认)会在 Undo 表空间超过 innodb_max_undo_log_size 时自动截断。前提是没有长时间运行的事务阻塞。

Q:发现历史列表很长怎么排查?
A:先查 INFORMATION_SCHEMA.INNODB_TRX 看有没有长事务,重点看 trx_started 字段。找到后评估能否安全 KILL。如果确认是事务问题,KILL 后 Purge 线程会自动清理。

Q:Undo 表空间文件可以删除吗?
A:绝对不能手动删除!必须通过 InnoDB 的自动或手动 TRUNCATE 机制。手动删除会导致 MySQL 崩溃或无法启动。


优化 InnoDB 刷脏页行为

优化 InnoDB 刷脏页行为

什么是脏页

在 InnoDB 中,脏页(Dirty Page)是指 Buffer Pool 中的数据页已经被修改,但还没有写回磁盘。

事务提交 → 修改 Buffer Pool 中的页(变成脏页)
        → 写入 Redo Log(持久化保证)
        → 后台线程异步将脏页刷回磁盘
        → 脏页变干净

脏页本身不是问题——这是 InnoDB 的设计核心(先在内存改,批量刷盘)。问题在于脏页过多或刷脏行为失控

为什么需要关注刷脏页

脏页比例过高会导致:

  1. Checkpoint 阻塞:Redo Log 写满后必须强制刷脏,导致用户查询被阻塞
  2. IO 尖刺:大量脏页集中刷新,磁盘 IO 瞬间打满
  3. 性能抖动:平时查询 1ms,刷脏期间变成 100ms
  4. Buffer Pool 污染:新数据需要空间,必须淘汰脏页

核心参数

innodb_io_capacity

作用:告诉 InnoDB 磁盘的 IO 能力上限,控制刷脏页的最大 IOPS。

innodb_io_capacity = 200     # HDD 机械硬盘
innodb_io_capacity = 2000    # 普通 SATA SSD
innodb_io_capacity = 5000    # 企业级 NVMe SSD
  • 设得太低:脏页堆积,刷脏跟不上写入速度
  • 设得太高:刷脏占用过多 IO,影响正常查询
  • 建议设置为磁盘实际最大 IOPS 的 80%

innodb_io_capacity_max

作用:在紧急情况下(如脏页比例超过阈值),InnoDB 可以超过 innodb_io_capacity 进行刷脏,此参数定义上限。

innodb_io_capacity_max = 3000  # 建议为 io_capacity 的 1.5-2 倍

innodb_max_dirty_pages_pct

作用:Buffer Pool 中脏页比例的上限,超过这个值将开始刷脏。

innodb_max_dirty_pages_pct = 75   # 默认 75%,建议值 70-85
  • 设得太低(如 30%):频繁刷脏,增加 IO
  • 设得太高(如 90%):脏页堆积严重,Checkpoint 时 IO 突增

innodb_max_dirty_pages_pct_lwm

作用:脏页达到这个比例时,InnoDB 会启动主动刷脏,避免达到上限后被动猛刷。

innodb_max_dirty_pages_pct_lwm = 50  # 默认 10%(5.7+)
  • 设得高(如 50%):主动刷脏开始得晚,可能积累大量脏页
  • 设得低(如 10%):很早开始主动刷脏,IO 更平滑

innodb_adaptive_flushing

作用:自适应刷脏,让 InnoDB 根据 Redo Log 生成速度动态调整刷脏速度。

innodb_adaptive_flushing = ON   # 默认开启,强烈建议保持

innodb_flush_neighbors

作用:刷脏时是否把相邻的脏页也一起刷(预读式刷新)。

innodb_flush_neighbors = 0  # HDD = 1,SSD = 0(推荐)
  • HDD(1):相邻页一起刷,利用顺序读写优势
  • SSD(0):不需要寻道,独立刷即可,避免额外 IO

观察脏页状态

-- 脏页数量
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty';

-- 脏页比例(估算)
SELECT 
    Innodb_buffer_pool_pages_dirty / Innodb_buffer_pool_pages_total * 100 AS dirty_pct
FROM performance_schema.global_status
WHERE variable_name = 'Innodb_buffer_pool_pages_dirty';

-- 查看刷脏行为
SHOW ENGINE INNODB STATUS\G

在 InnoDB Status 中查看相关部分:

----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 8589934592
...
Pages read 152345, created 4321, written 89123
  **Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 0 / 1000**
  **Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s**
LRU len: 524287, unzip_LRU len: 0
**I/O sum[0]:cur[0], unzip sum[0]:cur[0]**

刷脏页的触发条件

  1. 定时触发:InnoDB 后台线程定期检查并刷脏
  2. 用户请求触发:查询需要读取不在 Buffer Pool 中的页,但 Buffer Pool 已满,需淘汰脏页
  3. Redo Log 写满:Checkpoint 推进,强制刷脏
  4. 脏页比例超过阈值innodb_max_dirty_pages_pct
  5. MySQL 正常关闭:在关闭前刷所有脏页
  6. 自适应触发innodb_adaptive_flushing 根据 Redo Log 生成速率动态调整

刷脏页性能调优实战

场景 1:写入量大的 OLTP 系统

innodb_io_capacity = 5000           # 根据 SSD 性能调整
innodb_io_capacity_max = 10000
innodb_max_dirty_pages_pct = 75     # 保持默认
innodb_max_dirty_pages_pct_lwm = 30 # 提前开始主动刷脏
innodb_adaptive_flushing = ON
innodb_flush_neighbors = 0          # SSD 不需要
innodb_log_file_size = 2G           # 加大 Redo Log,减少 Checkpoint

场景 2:夜间批量写入

innodb_io_capacity = 8000           # 可以放高一些
innodb_max_dirty_pages_pct = 80     # 允许更多脏页积累
innodb_log_file_size = 4G           # 大日志空间减少 Checkpoint

场景 3:读多写少

innodb_io_capacity = 1000           # 写入少,不需要太高 IOPS
innodb_max_dirty_pages_pct = 50     # 脏页很少,保持干净
innodb_flush_neighbors = 0

监控刷脏页对性能的影响

-- 查看等待 Checkpoint 的次数
SHOW GLOBAL STATUS LIKE 'Innodb_log_waits';
-- 如果这个值持续增加,说明 Redo Log 太小

-- 查看刷脏页线程的状态
SHOW GLOBAL STATUS LIKE 'Innodb_data_fsyncs';
SHOW GLOBAL STATUS LIKE 'Innodb_data_pending_fsyncs';

最佳实践总结

  1. 正确预估 IO 能力innodb_io_capacity 是调优基础,设错就全白搭
  2. SSD 关 neighborinnodb_flush_neighbors = 0
  3. 自适应保持开启innodb_adaptive_flushing = ON
  4. 监控脏页比例:目标维持 < 30%,否则说明刷脏速度跟不上写入
  5. Redo Log 留够空间:避免频繁 Checkpoint
  6. 写入峰值时适当宽松:允许更高脏页比例,让刷脏更平缓
  7. 使用 sysbench 压力测试:模拟峰值写入验证参数配置

面试常问题

Q:为什么 MySQL 用"先写 Redo Log 再刷脏页"的方式?
A:这是 WAL(Write-Ahead Logging)的核心思想。写入 Redo Log 是顺序写,速度极快;刷脏页是随机写,速度慢。先写日志确保事务持久化,后台慢慢刷脏页,兼顾了性能和持久性。

Q:脏页比例达到上限会怎么样?
A:InnoDB 会强制推进 Checkpoint,刷脏页腾出 Redo Log 空间。这段时间用户查询可能被阻塞,表现为响应时间突然变长。

Q:怎么判断刷脏页是不是系统瓶颈?
A:看 InnoDB Status 中 "log i/o" 部分的 pending writes;看磁盘 IO 利用率是否在写入峰值时打满;看脏页比例是否持续高于 50%;看 Innodb_log_waits 是否持续增长。


sync_binlog 配置详解:安全与性能的平衡

sync_binlog 配置详解:安全与性能的平衡

概述

sync_binlog 是 MySQL 控制 Binlog 刷盘策略的关键参数,它决定了事务提交时 Binlog 写入磁盘的频率。这个参数在数据安全性和写入性能之间做权衡。

sync_binlog 的三种取值

行为 安全性 性能
1 每次事务提交都 fsync Binlog 到磁盘 ✅ 最高
0 不主动 fsync,由 OS 决定何时刷盘 ❌ 最低 🚀 快
N(N>1) 每 N 个事务才 fsync 一次 ⚠️ 中间 ⚡ 较快

sync_binlog = 1(最安全,推荐)

sync_binlog = 1

每个事务提交时,Binlog 都会 fsync 到磁盘:

事务 T1 提交 → Binlog 写入 OS Cache → fsync 磁盘 ✅
事务 T2 提交 → Binlog 写入 OS Cache → fsync 磁盘 ✅
事务 T3 提交 → Binlog 写入 OS Cache → fsync 磁盘 ✅
...

安全性:事务一旦提交返回成功,Binlog 一定在磁盘上,即使 MySQL 或 OS 崩溃也不会丢失。

性能:每次提交都需要一次磁盘 fsync,在 SSD 上通常能承受几千 TPS。

sync_binlog = 0(最高性能,最低安全)

sync_binlog = 0

MySQL 不主动刷盘,完全交给操作系统:

事务 T1 提交 → Binlog 写入 OS Cache(内存)
事务 T2 提交 → Binlog 写入 OS Cache(内存)
事务 T3 提交 → Binlog 写入 OS Cache(内存)
...
(OS 在某个时间点自动刷盘,可能过几秒)

风险:如果 OS 崩溃,最近的若干个事务的 Binlog 会丢失,从库不会同步到这些数据。

MySQL 崩溃不影响:只要 OS 不崩溃,Binlog 在 OS Cache 中是安全的。

sync_binlog = N(批量刷盘)

sync_binlog = 100

每 N 个事务 fsync 一次:

事务 T1 提交 → Binlog 写 OS Cache(不刷盘)
事务 T2 提交 → Binlog 写 OS Cache(不刷盘)
...
事务 T100 提交 → Binlog 写 OS Cache + fsync ✅
事务 T101 提交 → Binlog 写 OS Cache(不刷盘)
...

风险:OS 崩溃时最多丢失 N-1 个事务的数据。
不适合的场景:如果 N=10000,OS 崩溃可能丢失 9999 个已提交事务,风险巨大。

不同组合的安全等级

最高安全等级(不丢数据)

innodb_flush_log_at_trx_commit = 1  -- Redo Log 每次提交刷盘
sync_binlog = 1                      -- Binlog 每次提交刷盘
  • Redo Log 和 Binlog 都在事务提交时落盘
  • 任何崩溃都不会丢已提交事务的数据
  • 这就是所谓的 "双 1" 配置

高性能等级(可能丢 1 秒数据)

innodb_flush_log_at_trx_commit = 2  -- Redo Log 每秒刷盘
sync_binlog = 0                      -- Binlog 依赖 OS 刷盘
  • TPS 可以提升 5-10 倍
  • OS 崩溃可能丢失 1-2 秒的数据
  • MySQL 崩溃不丢数据(Redo Log 在 OS Cache 中,但 MySQL 重启前 OS 会刷盘)

中间等级

innodb_flush_log_at_trx_commit = 1
sync_binlog = 1000
  • Redo Log 不丢(每次提交刷盘)
  • Binlog 最多丢 999 个事务
  • 适合对 Redo Log 安全要求高、对 Binlog 容忍度高的场景

性能测试参考

在普通 SSD 上的测试(8 核 16G,MySQL 8.0):

配置 TPS 安全性
双 1 ~3,000 ✅ 不丢数据
sync_binlog=0 + innodb_flush=2 ~25,000 ⚠️ 丢 1 秒数据
sync_binlog=100 + innodb_flush=1 ~8,000 ⚠️ 丢 99 个事务

生产环境建议

金融/支付/核心业务

# 双 1 配置 —— 安全第一
sync_binlog = 1
innodb_flush_log_at_trx_commit = 1

日志/非核心业务

# 平衡配置
sync_binlog = 0
innodb_flush_log_at_trx_commit = 2

大促/高并发临时调整

如果需要短时间内提升吞吐量,可临时降低安全性:

-- 会话级别临时调整
SET SESSION sync_binlog = 1000;
SET SESSION innodb_flush_log_at_trx_commit = 2;
-- 执行大批量操作
-- 完成后恢复
SET SESSION sync_binlog = 1;
SET SESSION innodb_flush_log_at_trx_commit = 1;

面试要点

  1. sync_binlog=1 = 最安全模式,每次提交都刷盘
  2. sync_binlog=0 = 最高性能模式,OS 决定刷盘时机
  3. sync_binlog=N = 中间模式,每 N 个事务刷盘一次
  4. 双 1 配置sync_binlog=1 + innodb_flush_log_at_trx_commit=1)是最安全的配置
  5. OS 崩溃 vs MySQL 崩溃:sync_binlog=0 只在 OS 崩溃时才丢数据
  6. 性能差距可达 5-10 倍,但安全第一的生产环境请用双 1

两阶段提交如何保证 Redo Log 与 Binlog 的一致性

两阶段提交如何保证 Redo Log 与 Binlog 的一致性

概述

两阶段提交(Two-Phase Commit)是 MySQL 内部用于协调 Redo Log(InnoDB 引擎)和 Binlog(Server 层)写入顺序的协议。它的核心目标:确保无论何时崩溃,Redo Log 和 Binlog 永远保持一致。

为什么需要"一致"?

没有一致性会导致严重后果:

场景一:Redo Log 有,Binlog 没有

崩溃发生在:Redo Log 已写 ✅ → 系统崩溃 ❌ → Binlog 未写 ❌

恢复后:
- 主库:事务已通过 Redo Log 恢复 ✅
- 从库:Binlog 中没有这个事务 ❌
→ 从库永远少了这条数据,主从不一致

场景二:Binlog 有,Redo Log 没有

崩溃发生在:Binlog 已写 ✅ → 系统崩溃 ❌ → Redo Log 未写 ❌

恢复后:
- 主库:事务没有在 Redo Log 中,未恢复 ❌
- 从库:从库已通过 Binlog 同步了该事务 ✅
→ 主库少了这条数据,主从不一致

两阶段提交的完整协议

事务提交
    │
    ├── Phase 1:Prepare 阶段
    │   ├── 写入 Undo Log(便于回滚)
    │   ├── 将 Redo Log 写入磁盘,状态 = PREPARE
    │   └── Redo Log 刷新到磁盘(fsync)
    │
    ├── Phase 2:Commit 阶段
    │   ├── 写入 Binlog,记录该事务的 XID
    │   ├── Binlog 刷新到磁盘(fsync)
    │   ├── 在 Redo Log 中标记该事务为 COMMIT
    │   └── 释放锁,提交完成
    │
    └── 事务成功

每一步做什么?

-- 假设执行:UPDATE users SET balance = 100 WHERE id = 1

BEGIN;

-- 1. 执行 SQL,在 Buffer Pool 修改数据,并记录 Undo Log
UPDATE users SET balance = 100 WHERE id = 1;

-- 2. Prepare 阶段
--    分配 XID = 123
--    写入 Redo Log:[XID=123, PREPARE, space=5, page=100, offset=800, data='100']
--    fsync Redo Log 到磁盘

-- 3. Commit 阶段(写 Binlog)
--    写入 Binlog:[XID=123] ... UPDATE `test`.`users` ... SET @3=100 WHERE @1=1
--    fsync Binlog 到磁盘

-- 4. 标记提交
--    更新 Redo Log:[XID=123, COMMIT]
--    释放锁

COMMIT;

崩溃恢复策略

恢复时扫描 Redo Log,使用一个简单但严谨的规则:

系统崩溃 → 重启
    │
    ├── 扫描 Redo Log 中所有 PREPARE 状态的事务
    │
    ├── 对每个 PREPARE 状态的事务:
    │       │
    │       ├── 检查 Binlog 中是否有该事务的 XID
    │       │       │
    │       │       ├── 如果 Binlog 中有该 XID ✅
    │       │       │    → 说明两阶段都执行了
    │       │       │    → 提交事务(标记 Redo Log 为 COMMIT)
    │       │       │    → 重做该事务的修改
    │       │       │
    │       │       └── 如果 Binlog 中没有该 XID ❌
    │       │            → 说明事务未完成提交
    │       │            → 回滚事务(使用 Undo Log)
    │       │
    │       └── 决策完成
    │
    └── 恢复结束,Redo Log 和 Binlog 一致

恢复逻辑的核心依据

PREPARE 状态事务 Binlog 中是否有该 XID 恢复动作 原理
提交 Binlog 写完后才会出现这种情况,数据安全
回滚 Binlog 还没写,事务未完全提交

关键判断依据: Binlog 写完了 → 事务已经落盘 → 提交;Binlog 没写完 → 事务没落盘 → 回滚。

group commit 优化

两阶段提交每个事务需要 2 次 fsync(Redo Log prepare + Binlog),MySQL 通过 group commit 合并:

无 group commit(每个事务独立):
T1: Prepare(fsync) → Binlog(fsync) → Commit
T2: Prepare(fsync) → Binlog(fsync) → Commit
T3: Prepare(fsync) → Binlog(fsync) → Commit
→ 6 次 fsync

有 group commit(合并刷盘):
T1 Prepare → T2 Prepare → T3 Prepare
    ↓ (合并 fsync Redo Log)
3 个事务一起 fsync Redo Log

T1 Binlog → T2 Binlog → T3 Binlog
    ↓ (合并 fsync Binlog)
3 个事务一起 fsync Binlog

T1 Commit → T2 Commit → T3 Commit
→ 2 次 fsync(而非 6 次)

面试要点

  1. 两阶段提交的目的:解决 Redo Log 和 Binlog 两个独立日志的一致性问题
  2. Phase 1(Prepare):写 Redo Log(prepare 状态)+ fsync
  3. Phase 2(Commit):写 Binlog + fsync + 标记 Redo Log 为 commit
  4. 崩溃恢复规则:Binlog 有这个 XID 就提交,没有就回滚
  5. XID 是连接两个日志的桥梁,唯一标识一个事务
  6. Group commit 优化了 fsync 次数,提升并发性能
  7. 一句话总结:prepare 保证 Redo 安全,检查 Binlog 决定提交还是回滚,最终 Redo 和 Binlog 永远对齐

Binlog STATEMENT 格式的风险与使用限制

Binlog STATEMENT 格式的风险与使用限制

概述

STATEMENT 格式是早期 MySQL 默认的 Binlog 格式,它记录的是执行的 SQL 语句本身。虽然日志量小,但存在大量可能导致主从数据不一致的风险场景。

STATEMENT 格式的核心风险

STATEMENT 格式记录的是 SQL 逻辑,而不是执行结果。从库通过重新执行同样的 SQL 来同步数据。问题在于:同样的 SQL 在不同环境下可能产生不同的结果。

常见的不一致场景

1. 非确定性函数

-- 主库执行
INSERT INTO logs(user_id, login_time) VALUES(1, NOW());

-- 从库执行同样的 SQL
INSERT INTO logs(user_id, login_time) VALUES(1, NOW());
-- 问题:从库的 NOW() 返回时间 ≠ 主库
-- 结果:主从数据不一致!

类似函数:
- NOW()CURRENT_TIMESTAMP()SYSDATE()
- RAND()UUID()(4.0 版本)
- USER()CURRENT_USER()
- CONNECTION_ID()

2. 自增列和 LIMIT

-- 主库执行
DELETE FROM users ORDER BY id LIMIT 1;
-- 删除的是 id 最小的那条记录

-- 问题:如果从库数据行顺序不同
-- 从库可能删除不同的记录!

类似场景:

-- 没有 ORDER BY 的 LIMIT 操作
UPDATE users SET status = 1 LIMIT 100;
-- 主库和从库更新的"前 100 行"可能不同

3. 存储过程和触发器

-- 主库
CREATE PROCEDURE update_balance(IN user_id INT)
BEGIN
    UPDATE users SET balance = balance * 1.1 WHERE id = user_id;
END;

CALL update_balance(1);

-- 问题:如果从库的存储过程被修改或不存在
-- 或者存储过程内部使用了 NOW() 等函数

4. 依赖表数据和顺序

-- 主库执行
UPDATE users SET rank = (
    SELECT COUNT(*) + 1 FROM users u2 WHERE u2.score > users.score
);

-- 从库执行同样的 SQL
-- 但如果主从之间有微小差异,排名计算就会不同

5. 特殊字符集和排序规则

-- 主库默认字符集 utf8mb4
SET NAMES utf8mb4;
INSERT INTO names VALUES('😀');  -- emoji

-- 从库默认字符集 latin1
-- 重放该 SQL 时报错或插入乱码

6. 系统变量差异

-- 主库
SET sql_mode = '';
UPDATE users SET balance = '100abc';  -- 隐式转换为 100

-- 从库
-- sql_mode = STRICT_TRANS_TABLES
-- 同样的 SQL 报错!主从不一致

风险汇总表

风险类型 示例 影响程度
非确定性函数 NOW(), RAND(), UUID() 🔴 高
无排序 LIMIT DELETE LIMIT 无 ORDER BY 🔴 高
存储过程函数 过程内使用非确定函数 🔴 高
字符集差异 utf8mb4 vs latin1 🟡 中
系统变量 sql_mode 不同 🟡 中
触发器 主从触发器定义不同 🟡 中
自增列 INSERT 自增值依赖 🟢 低(大多一致)

STATEMENT 的配置优化

如果因为特殊原因必须使用 STATEMENT,可以配置以下参数降低风险:

binlog_format = STATEMENT

# 当检测到非确定性语句时记录警告但不阻止
binlog_direct_non_transactional_updates = OFF

# 记录使用非确定性函数的使用情况
log_bin_trust_function_creators = 0  # 限制函数创建

STATEMENT 格式的唯一优势

优势 说明
日志量极小 一条 UPDATE 影响 1000 万行也只记一行 SQL
写入速度快 日志少,刷盘快
传输带宽低 主从之间传输量小
人类可读 直接看到执行的 SQL

何时仍可使用 STATEMENT

  • 测试环境:数据一致性要求不高
  • 只读从库且数据允许少量偏差
  • 所有 SQL 都经过严格审核,确保确定性
  • 表结构简单,无函数/存储过程

面试要点

  1. STATEMENT 格式的核心风险:同样的 SQL 在不同环境下可能产生不同结果
  2. 高风险操作NOW()RAND()、无排序 LIMIT、存储过程、触发器
  3. 字符集和 sql_mode 差异也会导致主从不一致
  4. STATEMENT 的唯一优势:日志量小、写入快
  5. MySQL 5.7+ 弃用 STATEMENT:默认改为更安全的 ROW 格式
  6. 推荐做法:不要为了性能牺牲数据一致性,用 ROW 格式 + binlog_row_image=minimal

Redo Log 与 Binlog 的区别与对比

Redo Log 与 Binlog 的区别与对比

概述

Redo Log 和 Binlog 是 MySQL 中最容易混淆的两个日志组件。虽然它们都记录数据变更,但在设计目标、记录方式、所属层级和使用场景上存在本质差异。

核心区别速览

对比维度 Redo Log Binlog
所属层级 InnoDB 引擎层 MySQL Server 层
日志类型 物理日志 逻辑日志
记录内容 页级物理修改 SQL 语句或行数据
写入时机 事务执行中逐步写入 事务提交时一次性写入
写入方式 循环写(固定大小) 追加写(可无限增长)
主要用途 崩溃恢复(Crash Recovery) 复制 + PITR
是否可关闭 不可关闭(InnoDB 必需) 可关闭(不配置则无)
刷盘策略 innodb_flush_log_at_trx_commit sync_binlog
存储位置 ib_logfile 文件 mysql-bin.xxxxxx 文件
安全级别 不记录所有数据,只记录变化 记录完整的变更记录

逐项深入对比

1. 所属层级

MySQL Server 层(Binlog)
    │
    ├── InnoDB 引擎(Redo Log + Undo Log)
    ├── MyISAM 引擎(无 Redo Log,无事务)
    └── Memory 引擎(无持久化日志)
  • Redo Log 是 InnoDB 专有,其他存储引擎没有
  • Binlog 是所有引擎共用,Server 层统一管理

2. 日志类型

Redo Log(物理日志):

// 记录的是"第5号表空间、第100号页、偏移800处,写了'hello'"
REDO: space=5, page_no=100, offset=800, len=5, data='hello'

Binlog(逻辑日志):

-- 记录的是"执行了这条 SQL"
SET TIMESTAMP=...;
UPDATE users SET name = 'hello' WHERE id = 1;

3. 写入时机

BEGIN;
UPDATE users SET balance = 100 WHERE id = 1;
-- Redo Log 写入:巴拉巴拉巴拉(prepare)
UPDATE users SET balance = 200 WHERE id = 2;
-- Redo Log 写入:巴拉巴拉(prepare)
COMMIT;
-- Binlog 写入:一条事务记录
-- Redo Log 更新:prepare → commit

Redo Log:事务执行过程中逐步写入,可以支持回滚
Binlog:事务提交时一次性写入,是最终结果

4. 写入方式

Redo Log(循环写):

┌─────────────────────────────────────────────┐
│  [已清理] [活跃区域] [未使用] [已清理]        │
│            ↑ write         ↑ checkpoint      │
└─────────────────────────────────────────────┘
  • 固定大小,循环覆盖
  • innodb_log_file_size × innodb_log_files_in_group 总大小
  • 写满后会触发脏页刷盘(checkpoint)

Binlog(追加写):

mysql-bin.000001  → 满了 →  mysql-bin.000002  → 满了 →  mysql-bin.000003
     ↑ 顺序追加                   ↑ 顺序追加
  • 文件级增长,不覆盖
  • 需手动或自动清理过期文件
  • 文件大小由 max_binlog_size 控制

5. 刷盘策略对比

事务提交时的 I/O 操作:

sync_binlog=1 + innodb_flush_log_at_trx_commit=1 (最安全)
    1. Redo Log 刷盘(prepare)
    2. Binlog 刷盘
    3. Redo Log 刷盘(commit)
    → 最多 3 次 fsync,绝对安全

sync_binlog=0 + innodb_flush_log_at_trx_commit=2 (高性能)
    1. Redo Log 写 OS Cache
    2. Binlog 写 OS Cache
    → 0 次 fsync,OS 崩溃可能丢数据

6. 作用范围

功能场景 Redo Log Binlog
系统崩溃后恢复未刷盘的数据 ✅ 核心功能
主从复制 ✅ 核心功能
误操作恢复(闪回) ✅(ROW 格式)
数据审计
数据库时间点恢复
事务回滚 ❌(配合 Undo Log)

小测试

-- 问题:以下场景用哪个日志?
-- 1. MySQL 突然断电重启,某条已提交的数据丢了 → Redo Log
-- 2. 要从备份恢复到昨天下午 3 点的状态 → Binlog
-- 3. 从库要同步主库的数据 → Binlog
-- 4. 事务回滚 → Undo Log(不是 Redo 也不是 Binlog)
-- 5. 查看谁在什么时间改了哪些数据 → Binlog

面试要点

  1. Redo Log ≈ 物理的、引擎层的、崩溃恢复用的
  2. Binlog ≈ 逻辑的、Server 层的、复制恢复用的
  3. Redo Log 是循环写的(固定大小),Binlog 是追加写的(可增长)
  4. Redo Log 记录"怎么改的"(字节级),Binlog 记录"改了什么"(行级或 SQL)
  5. 两阶段提交协调两者:先写 Redo Log(prepare)→ 写 Binlog → 写 Redo Log(commit)
  6. 面试高频问题:"Redo Log 和 Binlog 的区别"——从层级、内容、写入方式、用途四个维度回答

Binlog ROW 格式的优缺点及适用场景

Binlog ROW 格式的优缺点及适用场景

概述

ROW 格式是 MySQL 5.7+ 默认的 Binlog 格式,它记录的是每行数据的实际变更内容,而不是执行的 SQL 语句。这使其在数据一致性上表现最优,但也带来了存储和性能上的代价。

ROW 格式的详细原理

记录内容

ROW 格式记录三个部分:

  1. 表映射(Table_map):记录哪个表的哪行数据被修改
  2. 变更前映像(Before Image):修改前的行数据
  3. 变更后映像(After Image):修改后的行数据
-- 原始 SQL
UPDATE users SET name = 'Bob' WHERE id = 1;

-- ROW 格式实际记录
Table_map: `test`.`users` mapped to number 89
Update_rows:
  BEFORE: @1=1 @2='Alice' @3=500   Before Image
  AFTER:  @1=1 @2='Bob'   @3=500   After Image

ROW 格式的镜像选项

binlog_row_image 参数控制记录哪些列:

选项 记录内容 日志大小 安全性
full 所有列都记录 Before 和 After 最大 最高
minimal 仅记录主键和变更列 最小(节省 30%-70%) 需主键
noblob 不记录 BLOB/TEXT 列 中等 看情况
# 推荐设置(节省空间且安全)
binlog_row_image = minimal

使用 minimal 时:

UPDATE users SET name = 'Bob' WHERE id = 1;
-- Before: @1=1 (仅记录主键)
-- After:  @1=1 @2='Bob' (记录主键和变更列)

优点

1. 数据绝对一致

无论执行什么 SQL,ROW 格式都能精确复制每一行的最终变化。

-- 即使是非确定性操作也不会出问题
UPDATE users SET create_time = NOW();       -- ✅ ROW 精确复制结果
UPDATE users SET balance = RAND() * 1000;  -- ✅ 没问题
DELETE FROM users ORDER BY RAND() LIMIT 1; -- ✅ 没问题

2. 支持闪回(Flashback)

基于 ROW 格式的 Binlog 可以实现数据恢复——将 After 当新数据,Before 当旧数据。

# 解析 ROW 格式 Binlog,反向操作实现闪回
mysqlbinlog --flashback mysql-bin.000001 | mysql -u root -p

# 原理:把 INSERT 转成 DELETE,DELETE 转成 INSERT,UPDATE 交换 Before/After

3. 从库执行效率高

从库直接应用行变化,不需要重新计算 SQL 执行计划。

4. DDL 也安全

DDL 操作虽记录为语句,但从库应用结果一致。

缺点

1. 日志量大

-- 批量操作时差异巨大
UPDATE users SET status = 1;  -- 100 万行

-- STATEMENT:只记录一行 SQL
SET TIMESTAMP=...; UPDATE users SET status = 1;

-- ROW:记录 100 万行的变更
Row 1: BEFORE(id=1, status=0) AFTER(id=1, status=1)
Row 2: BEFORE(id=2, status=0) AFTER(id=2, status=1)
...
-- 日志大小从几十字节 -> 几百 MB

2. 网络传输量大

Binlog 体积大导致主从之间的网络传输量增加,对带宽要求更高。

3. 无法直接查看 SQL

# 必须用 -v 参数解码
mysqlbinlog -v --base64-output=DECODE-ROWS mysql-bin.000001

4. 没有主键时性能差

ROW 格式在 WHERE 条件中选择行时,如果没有主键,每行都需要全表扫描匹配。有主键时通过主键直接定位。

适用场景

场景 推荐格式 原因
生产环境 ROW 数据一致性最重要
数据同步(Canal、Debezium) ROW 需要精确的行变更数据
闪回恢复 ROW ROW 支持 flashback
审计 ROW 记录精确的变更前/后数据
大数据量批量操作 可临时用 STATEMENT 减少日志量
只需了解执行的 SQL STATEMENT 或 MIXED ROW 不方便查看

面试要点

  1. ROW 格式 = 记录行数据变更,包括 Before Image 和 After Image
  2. 最大优点:数据绝对一致,任何非确定性操作都能正确复制
  3. 最大缺点:日志量大,批量操作尤为明显
  4. binlog_row_image:使用 minimal 可大幅减少日志量
  5. ROW 格式支持闪回:通过交换 Before 和 After 实现
  6. 生产环境推荐 ROW:数据一致性 > 空间/性能代价

Binlog(二进制日志):MySQL 数据复制的基石

Binlog(二进制日志):MySQL 数据复制的基石

概述

Binlog(Binary Log,二进制日志)是 MySQL Server 层维护的日志,记录了所有数据变更操作(DDL 和 DML)。它是 MySQL 数据复制时间点恢复(PITR) 的核心组件。

Binlog 的核心用途

1. 主从复制

主库 ── 产生 Binlog ──→ 从库 ── 读取 Binlog → 回放 Binlog
                                  ↑
                              I/O 线程拉取
                              SQL 线程回放

Binlog 是主从复制的数据源,从库通过读取和应用主库的 Binlog 来保持数据同步。

2. 时间点恢复(Point-in-Time Recovery)

# 从全量备份恢复
mysql -u root -p < full_backup.sql

# 应用 Binlog 恢复到崩溃前最后一刻
mysqlbinlog mysql-bin.000001 mysql-bin.000002 | mysql -u root -p

可以恢复到任意时间点:

# 恢复到指定时间点
mysqlbinlog --stop-datetime="2026-05-18 14:30:00" mysql-bin.000001 | mysql

# 恢复到指定位置
mysqlbinlog --stop-position=123456 mysql-bin.000001 | mysql

3. 审计

通过解析 Binlog 可以查看历史数据变更记录,用于数据审计。

Binlog 的启用与配置

# 必须配置 server-id(否则无法启用 Binlog)
server-id = 1

# 启用 Binlog(指定文件名前缀)
log_bin = /var/log/mysql/mysql-bin

# Binlog 格式
binlog_format = ROW

# 单个 Binlog 文件最大大小(默认 1G)
max_binlog_size = 1G

# Binlog 保留天数
expire_logs_days = 7
# 或者 MySQL 8.0 用
binlog_expire_logs_seconds = 604800

# 每次事务提交刷盘策略
sync_binlog = 1

Binlog 的文件组成

MySQL 会产生一系列 Binlog 文件:

mysql-bin.000001  ← 第一个 Binlog 文件
mysql-bin.000002  ← 第二个
mysql-bin.000003  ← 第三个
mysql-bin.index   ← Binlog 索引文件(记录有哪些 Binlog 文件)

查看 Binlog 文件列表

SHOW BINARY LOGS;
-- +------------------+-----------+
-- | Log_name         | File_size |
-- +------------------+-----------+
-- | mysql-bin.000001 |  1073741  |
-- | mysql-bin.000002 |  5242880  |
-- +------------------+-----------+

Binlog 的写入流程

事务提交
    │
    ├── 1. 事务修改记录写入 Binlog Cache(事务私有)
    │
    ├── 2. 事务提交时,Binlog Cache 刷入 Binlog 文件
    │
    ├── 3. Binlog 写入 OS Cache
    │
    └── 4. 根据 sync_binlog 配置刷盘

Binlog Cache 机制

  • 每个事务有自己的 Binlog Cache,存储在内存中
  • 如果事务很大,超过 binlog_cache_size,会使用临时文件
binlog_cache_size = 32768       -- 事务的 Binlog Cache 大小(默认 32KB)
max_binlog_cache_size = 2G      -- 单个事务最大 Binlog 大小
binlog_stmt_cache_size = 32768  -- 非事务表的 Binlog Cache

Binlog 与 Redo Log 的对比

维度 Binlog Redo Log
所属层级 Server 层(所有引擎共用) InnoDB 引擎层
日志类型 逻辑日志(SQL 或行记录) 物理日志(页修改)
写入时机 事务提交时写入 事务执行过程中逐步写入
写入方式 追加写,不循环 循环写,固定大小
用途 复制、PITR 崩溃恢复
是否可关闭 可关闭(不配置即可) 不可关闭(InnoDB 必备)
增长方式 文件级增长,可清理 循环覆盖
内容 SQL 语句或行数据 页级物理修改

管理 Binlog

-- 查看当前正在写入的 Binlog
SHOW MASTER STATUS;

-- 刷新 Binlog(产生新文件)
FLUSH LOGS;

-- 清理过期 Binlog
PURGE BINARY LOGS TO 'mysql-bin.000010';        -- 删除指定文件之前的
PURGE BINARY LOGS BEFORE '2026-01-01 00:00:00'; -- 删除指定时间之前的

-- 查看 Binlog 内容
SHOW BINLOG EVENTS IN 'mysql-bin.000001';

面试要点

  1. Binlog 是二进制格式,不直观,需要用 mysqlbinlog 工具查看
  2. Binlog 是追加写的,不会覆盖,可以无限增长(需定期清理)
  3. Binlog 记录的是"事务提交时"的数据,不是事务执行过程中
  4. Binlog 是 MySQL 主从复制的数据源,从库通过读取和回放 Binlog 保持同步
  5. PITR(时间点恢复) 依赖 Binlog,在全量备份基础上回放 Binlog 到指定时间点
  6. Binlog 是 Server 层日志,即使存储引擎不是 InnoDB(如 MyISAM)也能使用

Binlog 三种格式:STATEMENT、ROW 与 MIXED 详解

Binlog 三种格式:STATEMENT、ROW 与 MIXED 详解

概述

Binlog 有三种记录格式,每种格式在记录内容、空间占用、安全性上各有差异。选择合适的格式是保障 MySQL 复制安全和性能的重要决策。

三种格式概览

格式 记录内容 默认 MySQL 版本 空间占用 数据一致性
STATEMENT 执行的 SQL 语句 5.6 之前默认 最小 ⚠️ 不安全
ROW 实际修改的行数据 5.7+ 默认 最大 ✅ 安全
MIXED 混合策略 - 中等 视情况

STATEMENT 格式

记录执行的 SQL 语句本身:

-- 执行的 SQL
UPDATE users SET balance = balance + 100 WHERE age > 30;

-- Binlog 中的记录
# at 211
#2026-05-18 10:00:00 server id 1  end_log_pos 321  Query  thread_id=14
# exec_time=0  error_code=0
SET TIMESTAMP=1718600000/*!*/;
UPDATE users SET balance = balance + 100 WHERE age > 30
/*!*/;

优点:
- 日志量小,占用空间少
- 写入和传输速度快

缺点:
- 需要使用表结构信息执行 SQL,非确定性(Nondeterministic) 操作可能主从不一致
- 依赖函数、存储过程等上下文

ROW 格式

记录每一行数据的实际变更:

-- 执行的 SQL
UPDATE users SET balance = balance + 100 WHERE age > 30;

-- Binlog 中的记录(实际内容)
# at 211
#2026-05-18 10:00:00 server id 1  end_log_pos 321  Table_map: `test`.`users` mapped to number 89
# at 321
#2026-05-18 10:00:00 server id 1  end_log_pos 421  Update_rows: table id 89 flags: STMT_END_F

BINLOG '
--- 二进制格式,记录了具体的行变更 ---
# 修改前: id=1, name='Alice', balance=500, age=35
# 修改后: id=1, name='Alice', balance=600, age=35
# 修改前: id=2, name='Bob',   balance=200, age=40
# 修改后: id=2, name='Bob',   balance=300, age=40
'

优点:
- 数据绝对安全,主从不一致风险最低
- 任何操作都能正确复制,即使是非确定性函数

缺点:
- 日志量大(批量操作会产生大量行记录)
- 写入和传输速度较慢
- 无法直接看到 SQL(需要用 mysqlbinlog -v 解析)

查看 ROW 格式的 Binlog

# 解码 ROW 格式的 Binlog
mysqlbinlog -v --base64-output=DECODE-ROWS mysql-bin.000001

# 输出示例:
### UPDATE `test`.`users`
### WHERE
###   @1=1  -- id
###   @2='Alice'  -- name
###   @3=500      -- old balance
### SET
###   @3=600      -- new balance

MIXED 格式

MySQL 自动判断使用 STATEMENT 还是 ROW:

执行的 SQL
    │
    ├── 语句是确定性的(安全)→ STATEMENT 格式
    │   └── 如:UPDATE users SET name = 'fixed' WHERE id = 1;
    │
    └── 语句是非确定性的(不安全)→ ROW 格式
        └── 如:UPDATE users SET balance = NOW(); -- 包含函数

优点:
- 取长补短,平衡空间和安全
- 大部分场景使用 STATEMENT 节省空间

缺点:
- 判断逻辑复杂,仍有不确定性风险
- MySQL 对"确定性"的判断并不总是完美

格式对比表

对比项 STATEMENT ROW MIXED
日志大小 大(尤其批量操作)
复制安全性
资源消耗
是否支持所有函数 ❌(部分非确定函数出错) 大部分
DDL 记录 语句 语句 语句
批量操作 一条语句 N 条行记录 通常是 STATEMENT

生产环境推荐

# MySQL 5.7+ 默认已经是 ROW 格式
binlog_format = ROW

# 可以配合以下参数减少 ROW 格式的日志量
binlog_row_image = full   # 或 minimal, noblob

推荐使用 ROW 格式,原因:
1. 复制最安全
2. 磁盘现在已经便宜了(日志量不是主要矛盾)
3. 配合 binlog_row_image=minimal 可以减少日志量

面试要点

  1. STATEMENT ≈ 记录 SQL,日志小但不安全(函数、存储过程等可能导致主从不一致)
  2. ROW ≈ 记录行数据,日志大但最安全
  3. MIXED ≈ 让 MySQL 自己判断,但判断逻辑不完美
  4. MySQL 5.7+ 默认 ROW,生产环境推荐 ROW
  5. ROW 格式下,用 mysqlbinlog -v 解码查看具体内容
  6. DDL 操作 在任何格式下都记录为语句
© 版权声明
THE END
喜欢就支持一下吧
点赞13 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容