京东二面的这道题,我印象很深。面试官先抛出的是“加索引时,会锁表吗?”我当时顿了一下,因为这个问题真正要的答案不是“会”或“不会”,而是你对数据库、版本、DDL机制、锁模型的理解到了一个什么位置。后来复盘时我发现,如果只是机械背出“MySQL 5.6之后Online DDL不锁表”就能应付过去,但对面真正想听的,是你能不能讲清楚“为什么有的操作不锁、有的操作看起来卡死了、大表加索引到底该怎么落地”。这篇把那次面试之后我重新梳理的东西写全:从版本演进、Online DDL原理,到一次真实线上“假锁死”排查,再到大表加索引的工具选择和面试回答案例。
1. 这个问题的第一层答案:取决于你用的是哪个版本的MySQL
1.1 5.5及以前的真实情况:基本可以理解为锁表
MySQL 5.5时代,“给表加一个索引”并没有真正意义上的在线DDL能力。InnoDB虽然有Fast Index Creation这样的优化——添加二级索引时不需要把整张表拷贝成新表再替换,锁的范围还是会非常大。执行期间其他事务的写操作基本都要等,整张表被大粒度的锁包住,业务在这段时间内是做不了正常写入的。
如果用的是MyISAM存储引擎,那更直接,ALTER TABLE执行时会拿表的排他锁,读和写都会被挡住。早期互联网公司做表结构变更,都要挑凌晨流量最低的窗口,因为一次全表拷贝再换表可能跑几十分钟,期间业务写入中断是常态。现在很多老DBA习惯把变更放在凌晨2点到5点,这个习惯就是那个时代留下的。
1.2 5.6是真正的分水岭:Online DDL正式落地
MySQL 5.6把Online DDL从“特性预告”变成了“默认能力”。核心变化在于:ALTER TABLE在执行时需要明确选择算法和锁级别,同时也开始从机制上支持在执行阶段不阻塞业务写入。
ALTER TABLE t_order
ADD INDEX idx_user_id (user_id),
ALGORITHM=INPLACE,
LOCK=NONE;
对于“新增普通二级索引”这个操作,5.6之后是可以在INPLACE算法下,以LOCK=NONE的方式执行的。这意味着你在执行期间,业务可以继续对该表进行常规增删改查,不会被一块大锁挡住几十分钟。
1.3 版本对照:加索引时的锁行为差异
| 版本阶段 | 加普通二级索引时的表现 | 实际体验 |
|---|---|---|
| MySQL 5.5及以前 | 无法稳定支持Online DDL,结构变更需高强度锁保护,部分场景需拷贝整表 | 业务写入会中断,变更要低峰窗口 |
| MySQL 5.6 | Online DDL支持,ADD INDEX可使用ALGORITHM=INPLACE、LOCK=NONE | 执行阶段不阻塞读写,起止仍有短暂MDL锁 |
| MySQL 5.7 | 较5.6变化不大,锁机制延续并逐步完善 | 常见场景均可在线执行,但开始和提交阶段要注意 |
| MySQL 8.0 | 新增INSTANT算法,兼顾更多DDL场景加索引,仍以INPLACE为主 | 能力更强,但不代表所有操作都瞬间完成 |
所以,如果面试题只问“会锁表吗”,首答应该是一句话:MySQL 5.6之前,加索引可能锁表;5.6之后,在正常配置和语句条件下,加索引引起的阻塞被压缩到了一个非常小的窗口,并不会长时间锁表。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 拆开Online DDL:ALGORITHM和LOCK把锁细化到了哪个阶段
2.1 ALGORITHM=INPLACE不是完全不加锁,而是把锁集中在两个瞬间
很多人对“Online DDL不锁表”有个误解,以为整个执行过程业务完全无感。实际不是。
MySQL里执行一次ALTER TABLE,哪怕用了INPLACE,在操作最开始的准备阶段和最后提交阶段,都需要在表上获取一把MDL(元数据锁)的排他锁。这两个阶段每个通常只持续非常短的时间,可能只有几十毫秒到几百毫秒。可一旦系统里有长事务先把MDL读锁占住了,这张表在这个瞬间就是“被锁”的,后续所有请求都会排队。
官方对Online DDL的执行阶段描述可以简化为三步:
- 准备阶段:拿排他MDL锁,确认表结构状态,为后续变更初始化内部结构;
- 执行阶段:按照ALGORITHM来选择具体执行策略。如果是ADD INDEX且支持INPLACE,这一步不需要阻塞DML;
- 提交阶段:再次拿排他MDL锁,把新索引的元数据变更提交到数据字典,并处理可能存在的在线日志。
用现实生活类比:INPLACE加索引不像“全店停业装修”,更像“一边营业一边在商场中间加一部扶梯”,工人施工的同时顾客可以继续购物,但扶梯最终通电运行前,商场可能需要短暂封闭一小会儿完成最后接线。
2.2 LOCK子句到底在控制什么
ALTER TABLE的LOCK子句是给数据库的一条“底线”:
- LOCK=NONE:允许并发读和写,这是最轻的锁级别;
- LOCK=SHARED:允许并发读,但写入会被阻塞;
- LOCK=EXCLUSIVE:读和写都不允许。
如果你的操作本身不支持LOCK=NONE,你强行指定,MySQL会直接报错,而不是悄悄降级。比如:
ERROR 1846 (0A000): LOCK=NONE is not supported. Reason: COPY algorithm requires lock=SHARED.
这个报错信息已经告诉你原因:有的操作走的是COPY算法,这种算法本身需要更严格的锁来保证数据一致,因为它是把整张表一点点拷贝到一张新表里去,拷贝期间如果允许原表写入,新表就无法保证能追上这些变化。
2.3 为什么有些DDL至今还是要走COPY或者要锁
核心原因是物理存储格式限制。新增一个二级索引,本质上只是在原有数据不动的情况下,额外构建一棵B+树,所以能INPLACE。但如果要把某个字段从INT改成VARCHAR,行内数据长度会变化,原有数据页放不下,就必须把整行数据读出来,按新格式写入新表页,这种物理上的“行迁移”绕不开全表扫描和拷贝。
加索引和改列类型是两类完全不同的问题。很多面试者把“ALTER TABLE加索引”和“ALTER TABLE改结构”混为一谈,这会直接影响回答深度。普通索引的新增,由于只涉及“额外构建”而不是“全体重写”,天然适合Online方式执行;而修改列类型、修改字符集这类操作,哪怕在8.0也一样可能触发全表拷贝。
执行同一条ALTER语句时,MySQL自己会按一个顺序选择算法:优先INSTANT,其次INPLACE,最后COPY。这个选择逻辑和你指定的ALGORITHM参数互相约束。8.0里,如果某操作支持INSTANT算法,它会直接修改数据字典,既不重建表也不扫描表数据,速度极快——但很可惜,ADD INDEX目前不是INSTANT支持的范畴,它仍然需要INPLACE阶段。
2.4 8.0的INSTANT算法:和加索引有什么关系
MySQL 8.0.12以后开始支持INSTANT算法,目前主要用于“在表末尾新增列”这类操作,因为直接在元数据里追加一列,不需要重写数据页。对于加索引来说,INSTANT并不覆盖——二级索引的构建仍然需要读出相关字段值并排序,这一步无法绕过数据扫描。
这一点在面试时可以提一嘴:并不是MySQL 8.0之后所有加索引都瞬间完成,新算法扩大了不锁表的场景范围,但索引构建的扫描与排序成本并不会消失。
3. 线上一次“加索引把表卡死”的真实复盘
3.1 事发时看起来就是“锁表了”
有一回线上一个订单表要加索引,表大概3亿行。我用的是MySQL 5.7,语句写的是ALGORITHM=INPLACE, LOCK=NONE,理论上完全符合Online DDL条件。但执行后不到两分钟,业务方突然反馈写入耗时飙到十几秒,紧接着监控上出现大量“Waiting for table metadata lock”会话。
很多DBA第一次遇到这种情况都会懵——明明5.7、明明没锁表,为什么业务卡住了?
排查后发现,罪魁祸首不是ALTER语句本身的锁级别,而是这张表上有一个跑了很久的报表查询。那个查询持有了这张表上的MDL共享锁,我的ALTER语句要想开始执行,第一步就得拿到排他MDL锁。只要这个长查询不结束,ALTER就一直在准备阶段干等,进而阻塞了后面所有想继续拿共享MDL锁的DML。看起来是“加索引锁表”,实际是“加索引前的MDL等待卡住了后续所有请求”。
3.2 排查锁状态的具体命令
遇到类似情况,不要靠猜,直接用这几条命令定位:
SHOW FULL PROCESSLIST;
查看是否有大量会话处于“Waiting for table metadata lock”状态。
SELECT * FROM performance_schema.metadata_locks
WHERE OBJECT_SCHEMA = 'your_db'
AND OBJECT_NAME = 't_order'\G
找这张表上当前所有MDL锁的持有者,观察LOCK_STATUS是GRANTED还是PENDING。
SELECT * FROM sys.schema_table_lock_waits\G
如果sys库的视图可用,这条命令能直接看到阻塞关系。
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query
FROM information_schema.innodb_trx\G
确认是否有长时间未结束的事务和查询。
3.3 MDL锁等待还有个坑:新请求会连锁排队
MDL锁的排队机制有一个非常容易被忽略的地方:一旦有一个ALTER(要拿排他锁)在等待,它后面来的所有普通查询(要拿共享锁)也会排队,因为锁调度机制会优先满足排他锁请求,防止写请求饿死。
这会导致连锁反应:一条ALTER本意只是加索引,但它一旦因为某个慢查询而停在准备阶段,后面所有业务请求都会被一层层堵住,P99延迟瞬间恶化。这也是为什么有时候现象看起来和“锁表”一模一样。
3.4 线上处理办法和后续规避
当时我们的处理是:把那条跑了40多分钟的报表查询直接杀掉,ALTER随后很快就执行完了,业务恢复。这个操作简单,但要有流程支持,杀会话前需要和业务侧确认该查询可以中断。
事后我们总结了几条规避策略:
- 大表执行DDL前,先查innodb_trx和processlist,确认没有长事务;
- 给DDL设置lock_wait_timeout,避免无限等下去。比如SET SESSION lock_wait_timeout=30,如果30秒内拿不到MDL锁就主动失败,而不是默默卡住把整个库拖垮;
- 如果一台实例上经常有不可控的长查询,不要在业务高峰期直接执行ALTER,哪怕它理论上是Online的。
这个案例后来成了我面试时必讲的素材。它能证明:锁表不一定发生在“执行阶段”,也可能发生在“开始前那一下”。
4. 大表加索引,面试官真正想听的工程化答案
4.1 小表和中表:原生命令完全可以
先说结论,不要为了显得高级,一上来就套用工具。几十万行、上百万行的表,加索引实际上就是秒级到分钟级的事,直接用Online DDL原生语句执行就行,工具反而会带来复杂度。
我在生产环境通常这样判断:
- 表行数在千万以下,且实例负载不高,直接ALTER TABLE ... ALGORITHM=INPLACE, LOCK=NONE;
- 表行数在千万到亿级别,仍然可以用原生命令,但必须放到业务低峰,提前排查长事务;
- 表行数超过亿,或者主库写入压力大、从库延迟敏感,才考虑专门的在线DDL工具。
4.2 pt-online-schema-change:触发器方案的经典选手
Percona Toolkit里的pt-online-schema-change(pt-osc)是老牌工具。它的基本思路是:创建一张影子表,把原表结构改好,然后分批把数据从原表迁到影子表;同时用触发器把增量变更同步过去,最后用原子RENAME把新旧表切换。
pt-online-schema-change
--alter "ADD INDEX idx_user_id (user_id)"
D=app_db,t=t_order
--max-load Threads_running=30
--chunk-size=1000
--execute
pt-osc能很好地把大批量数据迁移分片执行,减少负载尖峰。但它有三个问题要注意:
- 触发器会增加原表DML的开销,因为每次增删改都要额外触发增量同步;
- 触发器在执行期间可能出现死锁,概率虽低但不能忽视;
- 表上没有主键或唯一键时,pt-osc无法使用,因为分片查找需要可靠的键。
4.3 gh-ost:基于Binlog的无触发器方案
gh-ost是GitHub开源的在线DDL工具,最大的特点是完全不依赖触发器,而是通过解析Binlog来获取增量数据。它把增量变更持续同步到影子表,再把原表和影子表交换,整个切换过程对主库依赖更小,也避免了触发器带来的额外事务开销。
gh-ost
--host=127.0.0.1
--user=ghost_user
--password=xxx
--database=app_db
--table=t_order
--alter="ADD INDEX idx_user_id (user_id)"
--allow-on-master
--max-load=Threads_running=30
--chunk-size=1000
--execute
gh-ost使用前提是必须开启Binlog,并且Binlog格式为ROW。它对账户权限要求也高,一般需要REPLICATION SLAVE、REPLICATION CLIENT等权限,在部分严格管控的数据库实例上,权限申请流程比较长。
4.4 三个方案如何选型
| 方案 | 核心机制 | 对业务影响 | 主要风险 | 适用场景 |
|---|---|---|---|---|
| 原生Online DDL | INPLACE/COPY/INSTANT | 起止有短暂MDL锁 | 准备阶段可能被长事务卡住 | 中小表、低峰期、快速变更 |
| pt-osc | 影子表+触发器 | 触发器增加DML开销 | 触发器死锁、无主键表不可用 | 大表、无锁表诉求强、环境无Binlog限制 |
| gh-ost | 影子表+Binlog同步 | 主库几乎无额外DML开销 | 依赖Binlog和较高权限 | 超大表、高写入场景、从库可分担 |
选型的核心逻辑不是“谁新用谁”,而是评估当前数据库实例的负载模型和运维约束。如果主库已经很高压,pt-osc的触发器开销会雪上加霜;但gh-ost需要Binlog权限,有些云数据库实例默认不开放这个权限,那就只能用原生或pt-osc。
4.5 我自己的经验:执行前后都必须留好观测手段
用任何工具之前,都要把磁盘空间、临时表空间、Binlog空间、CPU、IO延迟这几项基线记录下来。影子表方案通常需要额外的磁盘空间,大约等于原表体积的1.2到1.5倍;如果磁盘剩余不足,跑到一半工具会自动暂停甚至失败。
执行过程中要盯住三个指标:
- 主从复制延迟是否持续增长;
- Threads_running是否超过预设阈值;
- 是否存在DDL排队导致的MDL锁堆积。
我经历过不止一次:gh-ost跑得挺顺,结果主库的一个慢查询让复制延迟涨到几千秒,主从切换场景下这种操作会引发严重问题。所以所有工具都支持max-load、max-lag这类保护参数,一定要设,不能抱有侥幸心理。
5. 面试题最优回答思路:从版本、原理到实操,逐层递进
5.1 三层回答帮你把这道题转化成加分项
回答面试题不建议上来就背结论。我会这样组织答案:
第一层,直接回答核心:加索引是否会锁表,要看MySQL版本和具体操作。5.6之后加普通二级索引默认可以用Online DDL方式,不会长时间阻塞业务写入;5.6之前或者遇到不支持INPLACE的场景,仍然可能锁表,甚至需要拷贝整表。
第二层,说明执行流程:Online DDL并不是完全无锁,它在准备阶段和提交阶段需要短暂的MDL排他锁;执行阶段如果支持LOCK=NONE,普通DML可以继续进行。如果执行开始前表上已有长事务持MDL共享锁,ALTER会卡在准备阶段,引发连锁等待,表现为整个表上的业务全部堵死。
第三层,说明工程实践:对于亿级大表,我不会直接在业务高峰期执行原生命令,而是根据环境选择pt-osc或gh-ost,并通过max-load、lock_wait_timeout等参数控制风险;执行前排查长事务,执行中监控复制延迟和MDL状态。
这三层讲下来,面试官能明显感觉到你不是背题,而是真的在线上处理过这类问题。
5.2 面试官很可能接着追问的三个问题
追问一:如果ALTER一直卡住,怎么确认是MDL锁问题?
直接给排查命令:SHOW FULL PROCESSLIST看大量“Waiting for table metadata lock”,再用performance_schema.metadata_locks确认持有者和等待者。这一套答出来,说明你真有线上排查经验。
追问二:INPLACE执行期间并发写入是怎么保持一致性的?
可以解释为:Online DDL执行期间会把并发DML产生的增量记录存储在内存或临时空间中,超过阈值则可能扩大内存或报错;DDL完成后会应用这些增量日志,保证新索引包含最终一致的数据。这个过程中如果并发写入远超日志处理速度,DDL执行时间会被明显拉长,极端情况下会报online log相关错误。
追问三:既然Online DDL支持不锁表,为什么还有公司用pt-osc和gh-ost?
因为Online DDL在起止阶段仍有MDL锁需求,碰到长查询就可能卡住;且部分DDL不支持INPLACE。第三方工具通过影子表+触发器或Binlog同步,把变更影响削弱到更可控的程度,适合超大表和多从库环境。但工具也不是万能的,对触发器、权限、主键都有额外要求。
5.3 延伸一下:如果面试官聊到达梦数据库怎么查锁表
这类二面问题问到最后,面试官有时会延伸到国产数据库,比如“达梦数据库怎么查锁表”。达梦兼容Oracle的很多设计,锁查询可以从V$LOCK动态性能视图入手。常用的思路是查V$LOCK中BLOCKED字段为1的记录,找出正在等待锁的会话,再结合会话视图定位对应的SQL,和查MySQL里metadata_locks的思路本质上是一致的。
如果你在实际环境操作,建议先确认当前达梦版本的官方文档,不同版本里动态视图的字段命名会有细微差异。但核心排查逻辑是通用的:先看有没有锁等待,再找到阻塞者,评估该杀掉哪个会话。
如果你也被问到这道题,我建议别急着背“会”或“不会”,回去把自己生产库的版本、表行数、Binlog配置、主从架构都查一遍,开一个测试表,两边开两个session实际跑一次ALTER,用这文章里的命令观察MDL锁的变化。数据库的锁问题,看十篇博客不如自己实操一次——这套观测方法练熟了,再难的面试题也接得住。
