2421 字
约 8 分钟
19
S11-X04 · 给大表加索引会锁表吗:Online DDL 的真实边界
2024-12-31
2026-11-08

"加个索引而已,几秒钟"——这句话在 5.5 时代会换来一次全表只读。5.6 之后 Online DDL 让加二级索引可以不阻塞读写,但"不阻塞"前面有一串限定条件,条件不满足时 MySQL 会静默降级,或者卡在一个你看不见的锁上。以下按 MySQL 8.0 InnoDB 核对。


1. 三种 ALGORITHM

MySQL 执行 DDL 时会在三种算法里选:

ALTER TABLE t_order ADD KEY idx_status (status), ALGORITHM=INPLACE, LOCK=NONE;
算法 工作方式 是否重建表 并发 DML
INSTANT 只改元数据(8.0.12+ 部分操作) 否 允许
INPLACE 引擎层原地构建,不复制整表 否 视 LOCK 而定
COPY 建影子表、逐行复制、rename 是 阻塞

显式写 ALGORITHM=INPLACE 的意义是"失败就报错",而不是让 MySQL 悄悄退化成 COPY。不写算法时它自己挑,挑到 COPY 也不告诉你。


2. LOCK 的三级语义

级别 允许的并发操作 典型场景
LOCK=NONE 读 + 写全放行 加/删二级索引、改索引名
LOCK=SHARED 允许读,阻塞写 加全文索引、空间索引
LOCK=EXCLUSIVE 读写全阻塞 改列类型、COPY 类操作

三者不是并列选项,而是"声明你能接受的最差情况"。写 LOCK=NONE 而操作做不到,语句直接报 ER_ALTER_OPERATION_NOT_SUPPORTED,这是好事——比跑一半才发现锁表强。


3. 为什么加二级索引能 INPLACE + LOCK=NONE

加二级索引只需要:读聚簇索引 → 把索引列写进新的二级索引 → 处理这期间的增量。InnoDB 的做法是先标记索引为"创建中",对新增/删除的行记到 row log(innodb_online_alter_log_max_size 管这块内存),最后回放。

操作 算法 LOCK 备注
ADD INDEX / DROP INDEX INPLACE NONE 最常用的一类
ADD COLUMN(末尾、无默认值) INSTANT NONE 8.0.12+
MODIFY COLUMN 改类型 COPY EXCLUSIVE 必定锁表
ADD FOREIGN KEY INPLACE SHARED 需 foreign_key_checks=0 才更快
OPTIMIZE TABLE INPLACE NONE 实际是 rebuild,慎用

值得注意的是:加索引不重建表,但会读一遍全表。大表上这一遍的 IO 与 buffer pool 挤占,才是"慢"的来源,而不是锁。


4. 那个躲不掉的元数据锁窗口

DDL 开始与提交时都要拿 exclusive MDL(metadata lock)。这段窗口很短(毫秒到秒级),但它是"加索引也会卡住业务"的全部原因。

-- 会话 A:长事务,持有 t_order 的 MDL 读锁
BEGIN;
SELECT COUNT(*) FROM t_order WHERE status = 1;   -- 事务不提交

-- 会话 B:加索引,等 MDL 写锁
ALTER TABLE t_order ADD KEY idx_status (status), ALGORITHM=INPLACE, LOCK=NONE;

MDL 的规则是"读写互斥、写写互斥、读读共享"。会话 B 排队等写锁;此时会话 C 的普通 SELECT 来了,它需要 MDL 读锁——但 MySQL 为避免写锁饿死,会把 C 排在 B 后面。

于是:一个没提交的 SELECT + 一条 DDL = 整张表后续所有查询排队。表现是业务全站超时,SHOW PROCESSLIST 里一堆 Waiting for table metadata lock。


5. 阻塞链怎么读

SELECT id, user, db, command, time, state, LEFT(info, 60) AS info
FROM information_schema.processlist
WHERE state LIKE '%metadata lock%' OR command = 'Query'
ORDER BY time DESC LIMIT 20;
现象 含义
一条 ALTER 长时间 Waiting for table metadata lock 前面有未提交事务持读锁
一堆 SELECT 也停在 metadata lock 它们排在 DDL 之后(队列效应)
information_schema.innodb_trx 有 trx_state=RUNNING 且 trx_started 很早 就是它

真正的元凶往往是一个 autocommit=0 的连接做完查询没提交,或者一个连接池里泄漏的事务。找它比杀 DDL 更有效——杀 DDL 只解决一次,杀长事务解决根因。


6. 三个超时参数别搞混

参数 管什么 默认 建议
lock_wait_timeout MDL 等待时间(秒) 31536000 DDL 会话临时设 3–5
innodb_lock_wait_timeout 行锁等待时间(秒) 50 业务连接 3–10
innodb_online_alter_log_max_size Online DDL 增量日志上限 128M 写入高峰调大,否则报 1799
SET SESSION lock_wait_timeout = 5;
ALTER TABLE t_order ADD KEY idx_status (status), ALGORITHM=INPLACE, LOCK=NONE;

innodb_online_alter_log_max_size 被打满时,DDL 直接失败并回滚,报 ERROR 1799 (HY000): Creating index 'idx_status' required more than 'innodb_online_alter_log_max_size' bytes of modification log。这时该做的不是无限调大,而是换窗口或改用外部工具。


7. gh-ost 与 pt-online-schema-change

MySQL 原生 Online DDL 的两个硬伤:增量日志有上限;主库上的构建开销无法暂停。于是有了外部工具。

维度 gh-ost pt-osc
原理 伪装成从库读 binlog,影子表 + 切换 触发器同步增量,影子表 + rename
对主库写入影响 较小(binlog 解析) 触发器带来额外写开销
是否依赖触发器 否 是(部分业务禁用触发器)
暂停/限流 支持动态 --throttle 支持 --max-load、--critical-load
外键 支持较弱 支持较好
切换瞬间 需短暂锁表 rename 同样需 rename 锁
gh-ost \
  --host=db-primary --database=shop --table=t_order \
  --alter="ADD KEY idx_status (status)" \
  --allow-on-master --exact-rowcount \
  --max-load='Threads_running=30' \
  --critical-load='Threads_running=60' \
  --initially-drop-ghost-table --execute

取舍一句话:能用原生 ALGORITHM=INPLACE, LOCK=NONE 就别上工具;DDL 期间写入太猛、或需要可暂停可观测,才用 gh-ost(无触发器)或 pt-osc(外键多)。


8. 安全操作的五步

  1. 查长事务(最重要的一步):
SELECT trx_id, trx_state, trx_started,
       TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS age_sec,
       trx_mysql_thread_id, trx_rows_locked, LEFT(trx_query, 80) AS q
FROM information_schema.innodb_trx
ORDER BY trx_started ASC;

age_sec 超过几十秒的,先跟业务确认再处理;trx_mysql_thread_id 就是 KILL 的目标。

  1. 确认没人持有表级锁:SHOW OPEN TABLES WHERE In_use > 0; 与上面的 processlist 对照。
  2. 低峰执行,并显式声明算法与锁级别,让不支持时立刻失败。
  3. 设置会话级超时:SET SESSION lock_wait_timeout = 5;,宁可失败也不排队拖死业务。
  4. 观察与回滚预案:执行期间盯 Threads_running、Innodb_row_lock_waits;失败就 DROP INDEX(同样是 INPLACE + NONE),不留半成品。

9. 排错:五种卡住与报错

现象 原因 处理
DDL 一直 Waiting for table metadata lock 有未提交事务/长查询持 MDL 读锁 找 innodb_trx 定位并 KILL 该连接
业务全线超时,processlist 大量 metadata lock DDL 排在前面,后续查询被队列挡住 先杀 DDL,再杀长事务
ERROR 1205 锁等待超时 行锁冲突(innodb_lock_wait_timeout) 与 MDL 无关,看具体业务 SQL
ERROR 1799 增量日志超限 innodb_online_alter_log_max_size 太小或写入太猛 调大参数或换 gh-ost
DDL 报 ALGORITHM=COPY is required 操作本身不支持 INPLACE 接受锁表窗口,或用外部工具

10. 边界:什么时候锁表是必然的

操作 是否可避免锁表 说明
改列类型(INT → BIGINT) 否 必须重建,COPY + EXCLUSIVE
加列到表中间(AFTER col) 否 INSTANT 只支持末尾追加
修改字符集 否 全表重建
加唯一索引 近似可以 INPLACE,但重复值会导致失败
分区表重组 否 视分区操作而定
加普通二级索引 是 INPLACE + LOCK=NONE,仅剩短 MDL

最后提醒一句:LOCK=NONE 指的是不阻塞 DML,不是不消耗资源。大表加索引会读全表、写新索引页、抢 buffer pool,业务感受到的是"变慢"而不是"卡死",这两种故障的排查方向完全不同。


11. 组织级落地:把 DDL 变成一次发布

单个 DDL 安全,不代表一批 DDL 安全。把变更纳入发布流程,才有可重复的判断。

# 发布前自检:长事务 + 表大小 + 现有索引
mysql -N -e "SELECT table_name, table_rows,
             ROUND(data_length/1048576) AS data_mb
             FROM information_schema.tables
             WHERE table_schema='shop' ORDER BY data_length DESC LIMIT 10;"
变更规模 建议节奏
<1000 万行 原生 Online DDL,低峰直接执行
1000 万–1 亿行 原生 + 会话超时 + 执行前后盯 Threads_running
>1 亿行 gh-ost / pt-osc,可暂停可限流
需要改列类型 视同重建,必须有维护窗口
收尾动作 目的
ANALYZE TABLE 更新统计信息,避免新索引被优化器忽略
检查 SHOW INDEX 的 Cardinality 低于预期说明选择性差或统计过期
观察 Innodb_row_lock_waits 索引上线后锁等待应下降
保留 DROP INDEX 回滚语句 变更失败可逆

最容易被忽略的一步是 ANALYZE TABLE。 新索引刚建完统计信息往往不准,优化器可能继续选旧索引或全表扫,于是"加了索引没变快",然后被误判成索引失效。


12. 自测

  1. ALGORITHM=INPLACE 与 ALGORITHM=COPY 的核心差别是什么?
  2. 加二级索引为什么可以做到 LOCK=NONE?
  3. 一条 SELECT 没提交,为什么会让后面的 ALTER 和更多 SELECT 一起排队?
  4. lock_wait_timeout 和 innodb_lock_wait_timeout 分别管什么锁?
  5. DDL 报 ERROR 1799 说明哪个参数不够,正确做法是什么?
  6. 什么情况下应该放弃原生 Online DDL 改用 gh-ost?
S11-X04 · 给大表加索引会锁表吗:Online DDL 的真实边界
https://www.clxhxhhr.top/posts/5671/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。