InnoDB 加索引,这个时候会锁表吗?
结论:MySQL 5.6 起支持 Online DDL,给已有表加二级索引通常是
INPLACE+LOCK=NONE,执行期间不阻塞读写。但 DDL 开始与结束各需要一次排他的元数据锁(MDL),如果前面有长事务持着 MDL,ALTER会等待,并把它后面所有请求一起堵在队列里——这才是「加索引锁表」的真实成因。
一、三种 DDL 算法
| 算法 | 是否重建表 | 是否允许并发 DML | 典型操作 |
|---|---|---|---|
COPY | 是,拷到临时表 | 否,全程锁写 | 改列类型、加主键、5.6 之前的所有 DDL |
INPLACE | 视操作而定 | 多数允许(LOCK=NONE) | 加/删二级索引、加列、改索引名 |
INSTANT | 否,只改元数据 | 是 | 8.0 末尾加列、改默认值、重命名列 |
加二级索引属于 INPLACE,只扫描现有数据构建索引,同时把并发 DML 的变更记到一块日志里,最后合并。
二、加索引到底发生了什么
ALTER TABLE t_order ADD INDEX idx_user_ctime (user_id, ctime), ALGORITHM=INPLACE;
① 准备:取 MDL 排他锁(极短),检查是否支持 INPLACE
│ 拿不到 → 进入 MDL 等待队列,后续所有 SQL 一起排队
▼
② 执行:降级为 MDL 共享锁,允许并发读写
├─ 扫描聚簇索引,构建新索引
└─ 期间 DML 的变更写入 online log(row log)
▼
③ 提交:重放 online log 补齐增量 → 再取一次 MDL 排他锁 → 切换元数据
阻塞点只有 ① 和 ③ 两次短暂的排他 MDL,执行阶段本身不阻塞 DML。所以「加索引锁表」通常不是索引构建慢,而是拿不到 MDL。
三、正确的写法
-- 显式声明算法与并发级别;不支持时直接报错,而不是悄悄退化成 COPY
ALTER TABLE t_order
ADD INDEX idx_user_ctime (user_id, ctime),
ALGORITHM = INPLACE,
LOCK = NONE;
-- 报错示例(该操作不支持在线执行):
-- ERROR 1846 (0A000): ALGORITHM=INPLACE is not supported. Reason: ...
先把长事务清掉,再执行 DDL:
-- 找出运行超过 60 秒的事务
SELECT trx_id, trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS secs,
trx_mysql_thread_id, trx_query
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60
ORDER BY trx_started;
-- 限制 DDL 等 MDL 的时间,避免把业务拖死
SET SESSION lock_wait_timeout = 5;
-- 观察 MDL 等待链
SELECT * FROM performance_schema.metadata_locks
WHERE object_name = 't_order';
大表(千万级以上)更稳的做法是用影子表工具:
# gh-ost:无触发器、可限流、可暂停
gh-ost --host=127.0.0.1 --database=demo --table=t_order \
--alter="ADD INDEX idx_user_ctime (user_id, ctime)" \
--max-load='Threads_running=30' --critical-load='Threads_running=80' \
--allow-on-master --execute
四、哪些操作仍然会锁表
| 操作 | 算法 | 并发 DML | 说明 |
|---|---|---|---|
| 加二级索引 | INPLACE | 允许 | 需两次短 MDL |
| 加主键 | INPLACE | 不允许 | 要重建聚簇索引 |
| 改列类型 | COPY | 不允许 | 全表重写 |
| 加全文索引 | INPLACE | 不允许(5.7/8.0 部分版本) | 构建倒排 |
| 加外键 | COPY | 不允许 | 需校验子表 |
| 末尾加列 | INSTANT | 允许 | 8.0.29+ 支持任意位置 |
| 改字符集 | COPY | 不允许 | 全表重写 |
五、为什么线上感觉「锁了很久」
- 前面的长事务持着 MDL 共享锁:
ALTER在队列头等待,后续所有查询排在它后面,形成雪崩。 - DDL 自身耗时长:亿级表建索引可能几十分钟,期间虽然不锁 DML,但 IO 与 CPU 被大量占用。
- 主从延迟:DDL 在主库执行后同步到从库,从库重放期间复制延迟上升。
ALGORITHM未显式指定:某些版本会静默降级为COPY,此时全程锁写。
常见追问
- 加索引会锁表吗? 执行阶段不锁,但开始与结束需要极短的排他 MDL;拿不到就会等。
LOCK=NONE一定成功吗? 不一定,操作本身不支持时会报 1846;显式指定正是为了尽早暴露。- MDL 为什么这么危险? 它在语句开始时就申请,长事务持有期间 DDL 只能排队,而排队会阻塞后续所有语句。
- 8.0 的
INSTANT能加索引吗? 不能,INSTANT只支持元数据类变更;加索引仍需INPLACE构建。 - 从库上执行 DDL 有什么不同? 从库回放 DDL 时会停止 SQL 线程,大表 DDL 会造成明显延迟。
- 如何评估 DDL 的影响? 先在影子库用同量级数据试跑,记录耗时与 IO;线上用
gh-ost限流执行。
小结
「加索引锁表」是 5.5 时代的旧结论。现在加二级索引走 INPLACE,执行期间可读写,真正的风险是 MDL 等待被长事务放大。落地要点:显式写 ALGORITHM=INPLACE, LOCK=NONE、先清长事务、设置 lock_wait_timeout、大表走 gh-ost。