mysql大表结构变更导致主从延迟问题复盘

大表结构变更导致主从延迟问题复盘

时间: 2026-04-23 夜间
影响: 从库主从复制延迟,读请求堆积


一、背景

业务需要对大表 table_xxx 进行结构变更,采用 DMS 无损变更方案。

二、DMS 无损变更原理

sql 复制代码
┌──────────────────────────────────────────────────────────┐
│  1. 创建影子表 (ghost table)                              │
│     CREATE TABLE _table_xxx_new LIKE table_xxx;          │
│     ALTER TABLE _table_xxx_new ADD COLUMN ...;           │
├──────────────────────────────────────────────────────────┤
│  2. 增量数据同步                                          │
│     通过 binlog 或触发器将变更同步到影子表                 │
├──────────────────────────────────────────────────────────┤
│  3. 原子切换 (cut-over)                                   │
│     RENAME TABLE table_xxx TO _table_xxx_old,            │
│                  _table_xxx_new TO table_xxx;            │
└──────────────────────────────────────────────────────────┘

三、问题现象

变更执行后,监控发现:

  1. 从库主从复制延迟持续增长
  2. 从库出现大量 Waiting for table metadata lock 状态的查询
  3. 应用层读请求超时/报错

示例阻塞 SQL:

sql 复制代码
-- State: Waiting for table metadata lock
SELECT id, field1, field2, ... 
FROM table_xxx 
WHERE primary_key = 'xxx' AND status = 20;

四、根因分析

4.1 MDL 锁机制

MySQL 5.5+ 引入 Metadata Lock (MDL) 保护表结构:

操作类型 MDL 锁类型 持有时长
SELECT SHARED_READ 事务结束
INSERT/UPDATE/DELETE SHARED_WRITE 事务结束
DDL (RENAME/ALTER/DROP) EXCLUSIVE 操作完成

关键特性:

  • 即使是普通 SELECT,MDL 也持有到事务结束(非语句结束)
  • EXCLUSIVE 锁必须等所有 SHARED 锁释放
  • MDL 采用公平排队机制

4.2 问题链条

sql 复制代码
┌─────────────────────────────────────────────────────────────┐
│  从库存在长事务                                              │
│  (慢查询 / 未提交事务 / autocommit=0)                        │
│  持有 table_xxx 的 MDL SHARED_READ                          │
└─────────────────────────────────────────────────────────────┘
                              ↓ 阻塞
┌─────────────────────────────────────────────────────────────┐
│  主库执行 RENAME TABLE,binlog 同步到从库                    │
│  从库 SQL 线程尝试获取 EXCLUSIVE MDL                         │
│  → 被长事务阻塞 → SQL 线程挂起                              │
└─────────────────────────────────────────────────────────────┘
                              ↓ 阻塞
┌─────────────────────────────────────────────────────────────┐
│  后续 binlog 无法回放                                        │
│  → 主从延迟持续增长                                          │
└─────────────────────────────────────────────────────────────┘
                              ↓ 同时
┌─────────────────────────────────────────────────────────────┐
│  新的读请求进入从库                                          │
│  想获取 SHARED_READ,但 EXCLUSIVE 在排队                     │
│  → 公平锁机制导致新请求也必须排队                            │
│  → 读请求堆积 → 应用超时                                    │
└─────────────────────────────────────────────────────────────┘

4.3 MDL 公平排队示意

vbnet 复制代码
时间轴:
─────────────────────────────────────────────────────────────►

长事务 (SHARED_READ):  ████████████████████████████████
RENAME (EXCLUSIVE):           ↓等待
                              ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
新 SELECT (SHARED):                ↓被 EXCLUSIVE 阻塞
                                   ░░░░░░░░░░░░░░░░░░

五、诊断方法

5.1 查看 MDL 锁等待

sql 复制代码
-- MySQL 5.7+
SELECT 
    t.PROCESSLIST_ID,
    t.PROCESSLIST_INFO,
    m.LOCK_TYPE,
    m.LOCK_STATUS,
    t.PROCESSLIST_TIME
FROM performance_schema.metadata_locks m
JOIN performance_schema.threads t 
    ON m.OWNER_THREAD_ID = t.THREAD_ID
WHERE m.OBJECT_NAME = 'table_xxx';

5.2 找出长事务

sql 复制代码
SELECT 
    trx_mysql_thread_id,
    trx_started,
    TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_sec,
    trx_query
FROM information_schema.innodb_trx 
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60
ORDER BY trx_started ASC;

5.3 查看阻塞链

sql 复制代码
SELECT 
    p.ID,
    p.USER,
    p.HOST,
    p.TIME,
    p.STATE,
    LEFT(p.INFO, 100) AS sql_text,
    t.trx_started
FROM information_schema.PROCESSLIST p
LEFT JOIN information_schema.INNODB_TRX t 
    ON p.ID = t.trx_mysql_thread_id
WHERE p.COMMAND != 'Binlog Dump'
ORDER BY t.trx_started ASC, p.TIME DESC
LIMIT 20;

六、改进措施

6.1 变更前准备

措施 说明
检查从库长事务 确保无超过 N 秒的活跃事务
选择低峰期 避开读流量高峰
通知相关方 BI/报表等暂停大查询

6.2 从库配置优化

sql 复制代码
-- 设置锁等待超时,避免无限等待
SET GLOBAL lock_wait_timeout = 30;
SET GLOBAL innodb_lock_wait_timeout = 10;

-- 检查 autocommit 配置
SHOW VARIABLES LIKE 'autocommit';

6.3 自动化防护

bash 复制代码
# pt-kill 自动杀死从库长事务
pt-kill \
    --host slave-host \
    --match-info "SELECT" \
    --busy-time 60 \
    --kill \
    --print \
    --daemonize \
    --pid /var/run/pt-kill.pid

6.4 变更方案优化

方案 说明
gh-ost 有更优雅的 cut-over 重试机制
pt-osc 支持 --max-lag 自动限速
主动 kill 变更前在从库 kill 长事务

6.5 监控告警

  • 从库延迟 > 10s 告警
  • MDL 等待 > 30s 告警
  • 事务时长 > 60s 告警

七、经验总结

  1. MDL 锁是隐形杀手 --- 即使是普通 SELECT,只要在事务中就会持有 MDL 直到事务结束
  2. 从库长事务危害大 --- 会阻塞 DDL 回放,导致主从延迟
  3. 公平锁机制会放大影响 --- 一旦 EXCLUSIVE 排队,新读请求也会堆积
  4. 大表 DDL 需要全链路评估 --- 不仅关注主库,更要关注从库状态

文档版本: v1.0
编写日期: 2026-04-24

相关推荐
y = xⁿ2 小时前
MySQL学习日记:关于MVCC及一些八股总结
数据库·学习·mysql
BING_Algorithm2 小时前
JDBC核心教程
java·后端·mysql
ting94520003 小时前
MySQL 安全加固十大硬核操作
mysql
阿丰资源3 小时前
基于SpringBoot+MySQL的在线拍卖系统设计与实现(附源码)
spring boot·后端·mysql
牛马鸡niumasi3 小时前
MySQL:ENUM、SET与查询技巧
mysql
y = xⁿ3 小时前
MySQL八股总结:B+树的优势
b树·mysql
qiuyunoqy3 小时前
MySQL - 3 - mysqlcheck程序
数据库·mysql
黑牛儿4 小时前
MySQL主流存储引擎深度解析:优缺点对比+实操选型指南
数据库·mysql
咚咚王者4 小时前
MySQL 导出脚本
android·mysql·adb