问题

InnoDB加索引,这个时候会锁表吗?

答案

核心结论

取决于MySQL版本和使用的DDL方式:

  • MySQL 5.6之前:会长时间锁表,阻塞所有DML操作
  • MySQL 5.6+(Online DDL):只在开始和结束时短暂锁表,其余时间不锁表
  • MySQL 8.0+:进一步优化,某些操作甚至无需锁表

详细分析

1. MySQL 5.5及更早版本:全程锁表

执行方式:

ALTER TABLE users ADD INDEX idx_age(age);

锁表行为:

1. 加表级写锁(Table Lock)
   ↓
2. 创建临时表,按新结构
   ↓
3. 逐行复制数据到临时表(耗时长)
   期间:所有DML操作被阻塞
   ↓
4. 重命名表,删除原表
   ↓
5. 释放锁

影响:

  • 大表添加索引可能耗时数小时
  • 全程阻塞业务写入,导致服务不可用
  • 生产环境基本不可用

2. MySQL 5.6+:Online DDL(短暂锁表)

执行方式:

ALTER TABLE users ADD INDEX idx_age(age),
ALGORITHM=INPLACE, LOCK=NONE;

锁表行为:

阶段1:准备阶段
  - 加 MDL写锁(Metadata Lock,约1ms)
  - 初始化Online DDL结构
  - 降级为 MDL读锁

阶段2:执行阶段(主要耗时)
  - 持有 MDL读锁
  - 允许并发DML操作(SELECT、INSERT、UPDATE、DELETE)
  - 扫描主键索引,构建新索引
  - 将并发DML记录到 row log 缓冲区

阶段3:提交阶段
  - 升级为 MDL写锁(约数十ms)
  - 应用 row log 中的增量变更
  - 完成索引构建
  - 释放锁

关键点:

  • MDL读锁:允许DML操作,仅阻止其他DDL操作
  • MDL写锁:阻止所有DML和DDL,但仅持有极短时间
  • 不锁表的真正含义:大部分时间允许业务正常读写

示例时间线:

00:00:00.000  加MDL写锁
00:00:00.001  降级为MDL读锁
00:00:00.001  开始构建索引(业务正常运行)
00:01:30.000  索引构建完成
00:01:30.000  升级为MDL写锁
00:01:30.050  应用row log增量
00:01:30.050  释放锁,完成

总耗时:90秒
锁表时间:约51ms(0.001s + 0.050s)

3. MDL锁的作用

为什么需要MDL锁?

  • 保护表结构在DDL期间不被其他会话修改
  • 保证DDL操作的原子性

MDL锁的类型:

-- MDL读锁(SHARED_READ):允许DML,阻止DDL
SELECT * FROM users;  -- 加MDL读锁
INSERT INTO users VALUES (...);  -- 加MDL读锁

-- MDL写锁(EXCLUSIVE):阻止所有DML和DDL
ALTER TABLE users ADD INDEX idx_age(age);  -- 开始和结束时加MDL写锁

查看MDL锁:

-- MySQL 8.0
SELECT OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, OWNER_THREAD_ID
FROM performance_schema.metadata_locks
WHERE OBJECT_SCHEMA = 'test';

-- 输出示例
+---------------+-------------+-----------+-------------+-----------------+
| OBJECT_SCHEMA | OBJECT_NAME | LOCK_TYPE | LOCK_STATUS | OWNER_THREAD_ID |
+---------------+-------------+-----------+-------------+-----------------+
| test          | users       | SHARED_NO_READ_WRITE |  GRANTED  | 48   |
+---------------+-------------+-----------+-------------+-----------------+

4. 不同索引操作的锁表情况

操作 锁表时间 并发DML 说明
添加普通索引 短暂(ms级) 允许 INPLACE算法
添加唯一索引 短暂(ms级) 允许 INPLACE算法
添加全文索引 短暂(ms级) 允许 INPLACE算法
删除索引 短暂(ms级) 允许 INPLACE算法
添加主键 较长 部分允许 需重建表
删除主键 较长 阻塞 需重建表,COPY算法

示例:添加唯一索引

ALTER TABLE orders ADD UNIQUE INDEX uk_order_no(order_no),
ALGORITHM=INPLACE, LOCK=NONE;

-- 执行期间允许:
INSERT INTO orders VALUES (...);  -- 成功
UPDATE orders SET status = 1 WHERE id = 100;  -- 成功

-- 但如果插入重复值:
INSERT INTO orders VALUES (..., 'ORDER_001', ...);  -- 成功
INSERT INTO orders VALUES (..., 'ORDER_001', ...);  -- 等待DDL完成后报错

实际场景分析

场景1:低峰期添加索引

-- 凌晨2点执行
ALTER TABLE orders ADD INDEX idx_user_id(user_id);

-- 表现:
-- 1. 短暂阻塞(ms级):大部分请求不会感知
-- 2. 长时间执行(分钟级):但业务正常运行
-- 3. 磁盘IO升高:构建索引需要读取数据

场景2:高峰期添加索引

-- 中午12点执行
ALTER TABLE orders ADD INDEX idx_create_time(create_time);

-- 风险:
-- 1. MDL写锁等待:如果有长事务持有MDL读锁,升级MDL写锁会等待
-- 2. row log溢出:高并发DML可能导致row log缓冲区不足
-- 3. 主从延迟:DDL在从库重放时也会耗时

-- 建议:设置超时保护
SET SESSION lock_wait_timeout = 5;

场景3:有长事务阻塞DDL

-- 会话1:长事务(忘记提交)
BEGIN;
SELECT * FROM users WHERE id = 1;
-- 未提交,持有MDL读锁

-- 会话2:尝试添加索引
ALTER TABLE users ADD INDEX idx_age(age);
-- 等待会话1释放MDL读锁(阻塞)

-- 会话3:普通查询
SELECT * FROM users WHERE id = 2;
-- 也被阻塞(排在会话2之后)

排查方法:

-- 查看等待MDL锁的会话
SELECT * FROM sys.schema_table_lock_waits;

-- 杀掉长事务
KILL 12345;  -- 会话1的thread_id

避免锁表影响的最佳实践

1. 使用Online DDL显式指定

-- 明确指定INPLACE和LOCK=NONE
ALTER TABLE users ADD INDEX idx_email(email),
ALGORITHM=INPLACE, LOCK=NONE;

-- 如果不支持Online DDL,会报错而不是降级为COPY
-- ERROR 1846: LOCK=NONE is not supported. Reason: ...

2. 使用pt-online-schema-change

pt-online-schema-change \
  --alter "ADD INDEX idx_age(age)" \
  --max-load="Threads_running=50" \
  --critical-load="Threads_running=100" \
  --execute \
  D=mydb,t=users

优势:

  • 分批复制数据,避免长时间持有锁
  • 自动控制负载,业务影响更小
  • 可随时暂停、恢复

3. 低峰期执行

-- 定时任务,凌晨执行
# crontab
0 2 * * * mysql -e "ALTER TABLE mydb.users ADD INDEX idx_age(age)" >> /var/log/ddl.log 2>&1

4. 监控和告警

-- 监控长时间DDL
SELECT * FROM information_schema.PROCESSLIST
WHERE Command = 'Query' AND Time > 60 AND Info LIKE 'ALTER%';

-- 监控MDL等待
SELECT OBJECT_NAME, LOCK_TYPE, LOCK_DURATION, LOCK_STATUS
FROM performance_schema.metadata_locks
WHERE LOCK_STATUS = 'PENDING';

面试答题总结

InnoDB添加索引在MySQL 5.6+版本不会长时间锁表。通过Online DDL机制,仅在开始和结束时短暂持有MDL写锁(毫秒级),大部分时间持有MDL读锁,允许并发DML操作。具体流程是:短暂加MDL写锁 → 降级为MDL读锁并构建索引 → 升级为MDL写锁应用增量 → 释放锁。需注意长事务可能阻塞MDL锁升级,生产环境建议使用ALGORITHM=INPLACE, LOCK=NONE显式指定,或使用pt-osc工具在低峰期执行。