前言

在 MySQL 备份实践中,mysqldump --single-transaction --master-data=2 是最常见的组合。但很多 DBA 只知道"这样用不锁表",却不清楚底层原理。今天我们深入剖析这两个参数的工作机制,以及为什么它们必须配合使用。


一、MVCC 可见性规则(理论基础)

在理解 --single-transaction 之前,必须先掌握 InnoDB 的 MVCC 机制。

1.1 Read View 结构

当事务执行 START TRANSACTION WITH CONSISTENT SNAPSHOT 时,InnoDB 会创建一个 Read View:

1
2
3
4
5
6
7
struct read_view_t {
    trx_id_t m_low_limit_id;      // 系统中尚未分配的最小事务ID(当前最大事务ID + 1)
    trx_id_t m_up_limit_id;       // 活跃事务列表中的最小事务ID
    trx_id_t m_creator_trx_id;    // 创建此 Read View 的事务ID
    trx_id_t *m_ids;              // 创建 Read View 时的活跃事务ID数组(降序)
    ulint m_n_trx_ids;            // 活跃事务数量
};

注意:InnoDB 的命名反直觉

  • m_low_limit_id 虽然叫 “low limit”,但实际是最大值(下一个要分配的事务ID)
  • m_up_limit_id 虽然叫 “up limit”,但实际是最小值(最小活跃事务ID)

1.2 可见性判断规则

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def is_visible(row_trx_id, read_view):
    # 规则1:如果是当前事务自己修改的,可见
    if row_trx_id == read_view.m_creator_trx_id:
        return True

    # 规则2:如果行的事务ID < m_up_limit_id(最小活跃事务ID),说明在 Read View 创建前已提交,可见
    if row_trx_id < read_view.m_up_limit_id:
        return True

    # 规则3:如果行的事务ID >= m_low_limit_id(下一个要分配的ID),说明在 Read View 创建后才开始,不可见
    if row_trx_id >= read_view.m_low_limit_id:
        return False

    # 规则4:如果在 [m_up_limit_id, m_low_limit_id) 区间内
    # 需要检查是否在活跃事务列表 m_ids 中
    if row_trx_id in read_view.m_ids:
        return False  # 在活跃列表中,说明创建 Read View 时未提交,不可见
    else:
        return True   # 不在活跃列表中,说明已提交,可见

可见性判断示例

假设创建 Read View 时:

  • 已提交事务:trx_1 到 trx_99
  • 活跃事务:trx_100, trx_102, trx_105
  • 下一个事务ID:trx_106

则 Read View 为:

1
2
3
4
5
6
{
    m_up_limit_id: 100,        # 最小活跃事务ID
    m_low_limit_id: 106,       # 下一个要分配的ID
    m_ids: [105, 102, 100],    # 活跃事务列表(降序)
    m_n_trx_ids: 3
}

判断各行可见性:

  • row_trx_id = 95:95 < 100 → 可见(规则2,Read View 创建前已提交)
  • row_trx_id = 100:100 在 m_ids 中 → 不可见(规则4,未提交)
  • row_trx_id = 101:101 不在 m_ids 中 → 可见(规则4,已提交)
  • row_trx_id = 107:107 >= 106 → 不可见(规则3,Read View 创建后才开始)

1.3 Undo Log 回滚链

如果当前版本不可见,InnoDB 通过 roll_ptr 指针回溯历史版本:

当前版本 (trx_id=107, 不可见)
    ↓ roll_ptr
历史版本 (trx_id=101, 可见) ← mysqldump 读取这个版本
    ↓ roll_ptr
更早版本 (trx_id=95)

Undo Log 记录结构

Undo Log Entry {
    trx_id: 107
    undo_type: UPDATE
    table_id: 123
    old_values: {id: 1, name: "旧值"}
    roll_ptr: 指向更早的 undo log
}

二、--single-transaction 参数详解

2.1 功能定位

作用:利用 MVCC 创建一致性快照,导出过程不锁表

底层执行

1
2
3
START TRANSACTION WITH CONSISTENT SNAPSHOT;
-- 导出数据...
COMMIT;

2.2 工作原理

时间线示例

T0: 数据库正常运行
    - users 表有 100 条记录
    - 已提交事务:trx_1 到 trx_99
    - 活跃事务:trx_100, trx_102

T1: mysqldump 执行 START TRANSACTION WITH CONSISTENT SNAPSHOT
    - 创建 Read View
    - Read View = {
        m_up_limit_id: 100,        # 最小活跃事务ID
        m_low_limit_id: 103,       # 下一个要分配的ID
        m_ids: [102, 100],         # 活跃事务列表(降序)
        m_creator_trx_id: 101      # mysqldump 的事务ID
      }

T2: 其他事务提交并插入新数据
    - trx_103: INSERT INTO users VALUES (101, '张三');
    - trx_104: UPDATE users SET name='李四' WHERE id=1;

T3: mysqldump 导出数据
    - 读取 users 表
    - trx_103 的插入:trx_id=103 >= m_low_limit_id(103) → 不可见(规则3)
    - trx_104 的更新:trx_id=104 >= m_low_limit_id(103) → 不可见(规则3)
    - 导出结果:仍然是 100 条记录,id=1 的 name 是旧值

T4: mysqldump 完成
    - COMMIT

2.3 关键特性

优点

  • 不锁表,业务不受影响
  • 数据一致性:导出的是 T1 时刻的完整快照

缺点

  • 只适用于 InnoDB 表(MyISAM 不支持 MVCC)
  • 不知道快照对应哪个 binlog 位置

三、--master-data 参数详解

3.1 功能定位

作用:记录备份时刻对应的 binlog 位置,用于搭建主从复制

参数值

  • --master-data=1:在 dump 文件中写入 CHANGE MASTER TO 语句
  • --master-data=2:写入但注释掉(推荐,用于参考)

3.2 底层执行流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
-- Step 1: 获取全局读锁
FLUSH TABLES WITH READ LOCK;

-- Step 2: 记录 binlog 位置
SHOW MASTER STATUS;
-- 输出: mysql-bin.000123, Position: 456789

-- Step 3: 开启事务(创建 Read View)
START TRANSACTION WITH CONSISTENT SNAPSHOT;

-- Step 4: 立即释放全局锁
UNLOCK TABLES;

-- Step 5: 导出数据(使用 Step 3 的快照)
SELECT * FROM table1;
SELECT * FROM table2;
...

3.3 FTWRL 的作用

FLUSH TABLES WITH READ LOCK 做了什么?

  1. 阻止所有写操作:INSERT/UPDATE/DELETE/DDL 全部被阻塞
  2. 等待现有事务提交:确保所有未提交事务完成
  3. 创建一个"静止点":此时没有任何事务在执行

时间窗口

T1: FTWRL 执行 → 等待 trx_100, trx_102 提交
T2: 所有事务提交完成 → FTWRL 获取成功
T3: SHOW MASTER STATUS → 记录 binlog pos 456789
T4: START TRANSACTION → 创建 Read View
T5: UNLOCK TABLES → 释放锁(通常 < 1秒)

关键点:T2-T5 之间没有任何事务提交,保证 binlog 位置和 Read View 状态完全一致。

此时创建的 Read View

1
2
3
4
5
6
{
    m_up_limit_id: 103,        # 没有活跃事务,等于下一个ID
    m_low_limit_id: 103,       # 下一个要分配的ID
    m_ids: [],                 # 活跃事务列表为空
    m_creator_trx_id: 103      # mysqldump 的事务ID
}

因为 FTWRL 等待了所有事务提交,所以 Read View 的活跃列表为空,这是一个"干净"的快照。

四、为什么必须配合使用

4.1 单独使用的问题

问题1:只用 --single-transaction

备份结果:
✅ 得到一致性快照
❌ 不知道对应哪个 binlog 位置
❌ 无法搭建主从复制

具体场景

T1: mysqldump 创建 Read View(users 表 100 条记录)
T2: 其他事务插入 10 条记录,binlog 写到 pos 2000
T3: mysqldump 导出完成(dump 文件 100 条记录)

问题:从库应该从哪个 binlog 位置开始同步?
- 从 pos 2000?会丢失那 10 条记录
- 从更早的位置?可能重复应用已在 dump 中的数据
- 答案:不知道!❌

问题2:只用 --master-data

备份过程:
T1: FLUSH TABLES WITH READ LOCK
T2: SHOW MASTER STATUS
T3: 导出数据(持续持有全局锁)← 问题
T4: UNLOCK TABLES

问题:
❌ 导出期间所有写入被阻塞
❌ 如果数据库 100GB,导出需要 1 小时
❌ 业务停止 1 小时

4.2 核心问题:时间点对齐

目标公式

dump 文件的数据 + binlog(从某位置开始) = 完整数据

必须保证

dump 文件看到的数据状态 === binlog 位置记录的数据状态

4.3 错误场景分析

场景1:先记录 binlog,再创建快照

T1: SHOW MASTER STATUS → pos 1000
T2: 其他事务提交 → binlog 写到 pos 1500
T3: START TRANSACTION(Read View 包含 T2 的修改)
T4: 导出数据

结果:
- binlog pos 1000 不包含 T2
- dump 文件包含 T2
- 从库应用 binlog 时重复执行 T2 → 数据重复 ❌

具体例子

1
2
3
4
5
6
7
8
9
T1: 记录 binlog pos = 1000
T2: INSERT INTO users VALUES (1, '张三'); -- binlog 写到 pos 1500
T3: mysqldump 创建 Read View(包含 id=1
T4: 导出  dump 文件有 id=1

从库恢复:
1. 导入 dump  users  id=1
2.  pos 1000 应用 binlog  再次 INSERT id=1
3. 主键冲突 

场景2:先创建快照,再记录 binlog

T1: START TRANSACTION(创建 Read View)
T2: 其他事务提交 → binlog 写到 pos 1500
T3: SHOW MASTER STATUS → pos 1500
T4: 导出数据(看到的是 T1 的快照,不包含 T2)

结果:
- dump 文件不包含 T2
- binlog pos 1500 包含 T2
- 从库应用 binlog 时跳过 T2 → 数据丢失 ❌

具体例子

1
2
3
4
5
6
7
8
9
T1: mysqldump 创建 Read Viewusers  100 条)
T2: INSERT INTO users VALUES (101, '李四'); -- binlog 写到 pos 1500
T3: 记录 binlog pos = 1500
T4: 导出  dump 文件 100 

从库恢复:
1. 导入 dump  users  100 
2.  pos 1500 应用 binlog  跳过 id=101
3. 数据丢失 

4.4 正确流程:FTWRL 保证原子性

T1: FLUSH TABLES WITH READ LOCK
    ↓ 阻止写入,等待现有事务提交

T2: SHOW MASTER STATUS → pos 1500
    ↓ binlog 位置确定

T3: START TRANSACTION(创建 Read View)
    ↓ Read View 内容 === pos 1500 的状态

T4: UNLOCK TABLES
    ↓ 释放锁,业务恢复

T5: 导出数据(使用 T3 的 Read View)

关键:T2-T4 之间没有任何事务提交,binlog 位置和 Read View 状态完全一致。

五、深入理解 FTWRL

5.1 FTWRL 的等待机制

1
2
-- 如果有长事务未提交
FLUSH TABLES WITH READ LOCK;  -- 会等待所有事务提交

风险场景

T1: 用户执行 BEGIN; SELECT * FROM big_table; (未提交)
T2: mysqldump 执行 FTWRL → 等待 T1
T3: 所有其他写入被阻塞 → 生产故障

解决方案

1
2
3
4
5
6
7
-- 备份前检查长事务
SELECT trx_id, trx_started, trx_query
FROM information_schema.innodb_trx
WHERE trx_started < NOW() - INTERVAL 30 SECOND;

-- 必要时杀掉长事务
KILL <trx_mysql_thread_id>;

5.2 FTWRL 的锁范围

1
2
3
4
5
6
FLUSH TABLES WITH READ LOCK;
-- 此时:
-- ✅ 可以 SELECT
-- ❌ 不能 INSERT/UPDATE/DELETE
-- ❌ 不能 CREATE/DROP/ALTER
-- ❌ 不能 COMMIT(如果事务中有写操作)

六、参数功能总结

6.1 --single-transaction 的职责

解决的问题

  • ✅ 不锁表:利用 MVCC,业务不受影响
  • ✅ 一致性:导出的数据是某个时刻的完整快照

无法解决的问题

  • ❌ 不知道快照对应哪个 binlog 位置
  • ❌ 无法用于搭建主从复制

6.2 --master-data 的职责

解决的问题

  • ✅ 可追溯:记录快照对应的 binlog 位置
  • ✅ 可复制:从库可以从这个位置开始同步

无法解决的问题

  • ❌ 单独使用会长时间锁表
  • ❌ 需要 FTWRL 保证位置和快照一致

6.3 配合使用的价值

核心公式

dump 文件(快照) + binlog(从记录位置开始) = 完整数据

前提:快照时刻 === binlog 位置时刻
保证:FTWRL 的原子性操作

FTWRL 是桥梁:在短暂的全局锁期间,同时完成"记录位置"和"创建快照",保证两者对齐。

七、总结

7.1 核心要点

  1. --single-transaction 利用 MVCC 创建一致性快照,不锁表
  2. --master-data 记录 binlog 位置,用于主从复制
  3. FTWRL 是桥梁,保证 binlog 位置和 Read View 状态一致
  4. 两者必须配合,才能实现"不锁表 + 可复制"的完美备份

7.2 理解层次

初级 DBA:知道这样用不锁表 中级 DBA:理解 MVCC 快照和 binlog 位置 高级 DBA:掌握 Read View 可见性规则和 FTWRL 原子性保证

7.3 常见误区

误区1:认为 --single-transaction 完全不加锁 ✅ 真相:FTWRL 会短暂加全局锁(通常 < 1秒)

误区2:认为可以先备份再记录 binlog 位置 ✅ 真相:必须在 FTWRL 保护下同时完成,否则数据不一致

误区3:认为 MyISAM 表也能用 --single-transaction真相:MyISAM 不支持 MVCC,会退化为锁表备份

7.4 生产环境推荐命令

1
2
3
4
5
6
7
8
9
mysqldump \
  --single-transaction \
  --master-data=2 \
  --routines \
  --triggers \
  --events \
  --set-gtid-purged=AUTO \
  --default-character-set=utf8mb4 \
  --databases mydb > backup.sql

作者:DBA 随记 日期:2026-03-29 标签:mysqldump, 备份恢复, MVCC, 主从复制