只读实例上存在mdl锁等待,因其执行select等访问表操作时会自动持有mdl读锁(如shared_read),且该锁持续至事务结束或连接断开;当主库ddl在从库sql线程回放需获取mdl写锁时,即被这些未释放的读锁阻塞,导致复制卡在“waiting for table metadata lock”。

只读实例上为什么会有MDL锁等待
因为只读实例不是“只读”就免疫MDL锁——只要执行了任何访问表的语句(哪怕只是SELECT),MySQL就会自动加MDL读锁,而这个锁会一直持有着,直到事务结束或连接断开。主库上跑的ALTER TABLE到了从库,SQL线程需要申请MDL写锁,但被只读实例上未释放的读锁卡住,于是整个复制链就停在“Waiting for table metadata lock”。
常见触发场景:那些你以为“安全”的只读操作
只读实例上的SELECT本身不写数据,但极易成为MDL锁源头,尤其在以下情况:
- 应用连接池配置
autocommit=false,执行完SELECT * FROM order_logistics LIMIT 1后没COMMIT也没CLOSE,连接空闲挂着,MDLSHARED_READ锁照常持有 - 监控脚本或BI工具用长连接轮询,每分钟执行一次
SELECT COUNT(*) FROM t,但连接永不关闭,锁持续存在 - 慢查询没被及时
KILL,比如SELECT ... FROM view_order_logistics_new跑了60分钟,期间一直持有该表的MDL读锁 - ORM框架(如Django)在只读实例上启用了事务隔离(如
READ COMMITTED),一个SELECT隐式开启事务,后续没显式结束
为什么SHOW PROCESSLIST查不到持锁者
SHOW PROCESSLIST只显示当前正在执行的语句状态,而真正持锁的线程可能处于Sleep或Waiting for table metadata lock状态,已经不活跃了。必须查performance_schema.metadata_locks才能定位:
SELECT OBJECT_SCHEMA, OBJECT_NAME, LOCK_TYPE, LOCK_STATUS, THREAD_ID FROM performance_schema.metadata_locks WHERE OBJECT_SCHEMA = 'your_db' AND OBJECT_NAME = 'your_table' AND LOCK_STATUS = 'GRANTED';
重点关注LOCK_TYPE为SHARED_READ或SHARED_WRITE且LOCK_STATUS = 'GRANTED'的行——这些THREAD_ID就是真正在 hold 锁的连接。再关联performance_schema.threads查它的PROCESSLIST_INFO和PROCESSLIST_STATE,往往发现是空值或Sleep。
ALGORITHM=INPLACE也救不了只读实例的MDL阻塞
即使主库ALTER TABLE用了ALGORITHM=INPLACE,从库SQL线程在回放时仍需获取MDL写锁。它不关心主库怎么改的结构,只关心“此刻我能不能安全地更新这张表的元数据”。只要只读实例上有任何线程还攥着该表的MDL读锁,SQL线程就只能干等。更关键的是:ALGORITHM=INSTANT(MySQL 8.0.12+)虽然能跳过重建,但仍要求表无全文索引、无虚拟列——很多只读实例上的业务表并不满足这些条件,所以依然会触发MDL写锁等待。
最易被忽略的一点:只读实例上没有INNODB_TRX记录的“空闲连接”,照样能长期持有MDL锁;排查时别只盯着“正在运行”的线程,得去metadata_locks里翻“已授予但没人管”的锁。











