select被阻塞主因是它被其他事务持有的mdl或行锁阻塞,而非主动加锁;典型场景为autocommit=0事务未提交,导致后续alter table及所有查询排队等待。

SELECT 被阻塞,真不是它想锁别人,是它自己被卡住了
MySQL 中的 SELECT 本身不加行锁(默认隔离级别下),也不主动申请排他锁,但它在事务中执行时,会参与元数据锁(MDL)和行级锁的协同机制。一旦出现阻塞,大概率不是 SELECT 在“拦路”,而是它正等着别人释放锁——比如一个未提交的事务持有了 MDL 共享锁或行锁,而后续 DDL 或写操作又反过来卡住了它。最典型、最容易被忽略的场景就是:事务开着没关,SELECT 执行完就停在那里,结果整个表的 ALTER TABLE 和后续所有查询全被拖住。
为什么 autocommit=0 的事务会让 SELECT 成为“锁源”
关键点在于 MySQL 的 MDL 锁生命周期:只要事务没结束(没 COMMIT 或 ROLLBACK),它对表持有的 MDL 共享锁(MDL_SHARED_READ)就不会释放。而 ALTER TABLE 需要的是 MDL_EXCLUSIVE 锁,必须等所有 MDL_SHARED_* 锁全部释放才能上。
-
pymysql、mysql-connector-python等驱动默认autocommit=False,开连接即进事务模式 - 哪怕只执行一条
SELECT * FROM t,只要没显式提交,这个连接就一直持有 MDL 锁 - 此时另一个会话执行
ALTER TABLE t ADD COLUMN x INT,状态立刻变成Waiting for table metadata lock - 更糟的是,第三个会话再发
SELECT * FROM t,也会被卡住——因为ALTER拿不到独占锁,后面的请求全得排队等它
怎么快速定位谁在 hold 着 MDL 锁
不能只看 SHOW PROCESSLIST,它只显示“谁在等”,不显示“谁在占”。得查 performance_schema + information_schema 联合视图:
SELECT b.processlist_id AS blocker_id, c.user, c.host, c.db, c.command, c.time AS duration_sec, a.SQL_text AS blocking_sql FROM performance_schema.events_statements_current a JOIN performance_schema.threads b ON a.thread_id = b.thread_id JOIN information_schema.processlist c ON b.processlist_id = c.id JOIN information_schema.innodb_trx d ON c.id = d.trx_mysql_thread_id WHERE d.trx_state = 'RUNNING' AND d.trx_started <p>重点关注 <code>duration_sec</code> 大于几秒、且 <code>command</code> 是 <code>Sleep</code> 或 <code>Query</code> 但长时间没返回的记录——这往往就是那个忘了 <code>COMMIT</code> 的“幽灵事务”。</p><h3>SELECT FOR UPDATE / LOCK IN SHARE MODE 会加剧阻塞吗</h3><p>会,但原因不同:这类语句触发的是 InnoDB 行锁,不是 MDL。它们阻塞的是其他需要修改同一行的 <code>UPDATE</code>、<code>DELETE</code>,甚至同条件的 <code>SELECT ... FOR UPDATE</code>,但一般不影响普通 <code>SELECT</code>(除非隔离级别是 <code>SERIALIZABLE</code>)。</p>
-
SELECT ... FOR UPDATE在 RR 隔离级别下会对扫描到的索引记录加 next-key 锁,可能锁住间隙,导致其他事务插入/更新失败 - 如果查询没走索引,会升级为全表扫描+全表加锁,阻塞面极大
- 它不会直接阻塞
ALTER TABLE,但若该事务还同时持有 MDL(比如它也是 autocommit=0 开启的),那它就同时贡献了两种锁压力
真正容易被忽视的,是那些看似无害的长事务里的普通 SELECT ——它们不改数据,却悄悄锁着元信息,直到 DBA 收到告警才去翻日志。处理这类问题,比优化慢查询更急,也更隐蔽。











