在 MySQL 中执行数据定义语言(DDL)操作(如 ALTER TABLE)时,会获取元数据锁(MDL, Metadata Lock),这可能导致正常查询被阻塞。下面对此过程进行详细分析。
MDL 锁的基本原理
元数据锁(MDL)用于保护数据库对象的结构,确保在结构更改时不会有并发操作破坏数据一致性。
DDL 操作阻塞查询的过程
1. 正常状态下的查询
在正常情况下,查询会获取共享MDL锁(S锁),多个查询可以同时获取共享锁并行执行。
sequenceDiagram
participant A as 会话A(查询)
participant MDL as MDL锁系统
participant Table as 表
A->>MDL: 请求共享MDL锁(S锁)
MDL->>A: 授予共享锁
A->>Table: 执行SELECT查询
A->>MDL: 释放共享锁
2. DDL 操作请求排他锁
当执行 ALTER TABLE 等DDL操作时,会请求排他MDL锁(X锁)。此时:
- 新的DDL操作需要等待当前所有查询释放S锁
- 新的查询请求会被阻塞,无法获取S锁
sequenceDiagram
participant A as 会话A(查询)
participant B as 会话B(DDL操作)
participant C as 会话C(新查询)
participant MDL as MDL锁系统
participant Table as 表
A->>MDL: 获取共享MDL锁(S锁)
MDL->>A: 授予共享锁
Note over A,Table: 会话A正在执行长时间查询...
B->>MDL: 请求排他MDL锁(X锁)
Note over B,MDL: 会话B被阻塞(等待A释放S锁)
C->>MDL: 请求共享MDL锁(S锁)
Note over C,MDL: 会话C被阻塞(等待B的X锁请求处理完)
Note over A,C: 此时形成了锁等待队列
A->>MDL: 释放共享锁
MDL->>B: 授予排他锁
B->>Table: 执行ALTER TABLE
B->>MDL: 释放排他锁
MDL->>C: 授予共享锁
C->>Table: 执行SELECT查询
C->>MDL: 释放共享锁
3. MDL 锁阻塞的级联效应
级联阻塞效应:
graph TD
A[会话A: 持有共享MDL锁<br>长时间查询] --> B[会话B: 等待排他MDL锁<br>ALTER TABLE]
B --> C[会话C: 等待共享MDL锁<br>SELECT查询]
C --> D[会话D: 等待共享MDL锁<br>SELECT查询]
D --> E[会话E: 等待共享MDL锁<br>SELECT查询]
style A fill:#f9f,stroke:#333,stroke-width:2px
style B fill:#bbf,stroke:#333,stroke-width:2px
style C fill:#dfd,stroke:#333,stroke-width:2px
style D fill:#dfd,stroke:#333,stroke-width:2px
style E fill:#dfd,stroke:#333,stroke-width:2px
- 会话 A:长时间运行的查询持有S锁
- 会话 B:DDL操作等待X锁
- 会话 C、D、E:新查询请求S锁被阻塞在DDL后面
- 更多查询被阻塞,形成队列
实际案例分析
实验准备
首先创建一个测试表:
CREATE DATABASE mdl_test;
USE mdl_test;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入一些测试数据
INSERT INTO users (name, email) VALUES
('User 1', 'user1@example.com'),
('User 2', 'user2@example.com'),
('User 3', 'user3@example.com'),
('User 4', 'user4@example.com'),
('User 5', 'user5@example.com');
实验步骤
终端1(会话A):开启显式事务并执行查询,但不提交
USE mdl_test;
-- 开启事务
START TRANSACTION;
-- 执行一个查询,此时会获取MDL共享锁
SELECT * FROM users;
-- 重要:此时不要提交事务,保持事务开启状态
终端2(会话B):尝试执行DDL操作
USE mdl_test;
-- 尝试执行ALTER TABLE操作,此操作需要获取MDL排他锁
-- 由于会话A持有MDL共享锁,此操作会被阻塞
ALTER TABLE users ADD COLUMN address VARCHAR(200);
-- 此命令将会挂起等待
终端3(会话C):尝试执行普通查询
USE mdl_test;
-- 在会话B被阻塞后,尝试执行普通查询
-- 此查询也需要获取MDL共享锁,但因为会话B正在等待排他锁,所以会被阻塞
SELECT * FROM users WHERE id = 1;
-- 此命令将会挂起等待
终端4(会话D):查看当前的锁状态
-- 查看当前正在执行的查询
SHOW PROCESSLIST;
-- 查看元数据锁情况(MySQL 8.0+)
SELECT * FROM performance_schema.metadata_locks;
-- 查看锁等待情况
SELECT * FROM sys.schema_table_lock_waits;
终端1(会话A):提交事务释放锁
-- 回到终端1,提交事务,释放MDL共享锁
COMMIT;
如何诊断MDL锁阻塞
可以通过以下命令查看正在等待的锁:
-- 查看当前正在执行的进程
SHOW PROCESSLIST;
-- 查看元数据锁情况
SELECT * FROM performance_schema.metadata_locks;
-- 查看阻塞关系
SELECT
b.thread_id AS '被阻塞线程',
b.processlist_id AS '被阻塞会话ID',
b.processlist_info AS '被阻塞查询',
c.thread_id AS '阻塞线程',
c.processlist_id AS '阻塞会话ID',
c.processlist_info AS '阻塞查询'
FROM
performance_schema.metadata_locks a
JOIN performance_schema.threads b ON a.owner_thread_id = b.thread_id
JOIN performance_schema.metadata_locks m ON m.object_name = a.object_name
JOIN performance_schema.threads c ON m.owner_thread_id = c.thread_id
WHERE
a.lock_status = 'PENDING';
-- 查看等待事件
SELECT * FROM sys.schema_table_lock_waits;
-- 终止长时间运行的查询
KILL [阻塞会话ID];
-- 查看当前阻塞的MDL锁
SELECT
b.id AS blocked_id,
b.user AS blocked_user,
b.host AS blocked_host,
b.command AS blocked_command,
b.time AS blocked_time,
b.state AS blocked_state,
b.info AS blocked_query,
p.id AS blocking_id,
p.user AS blocking_user,
p.host AS blocking_host,
p.command AS blocking_command,
p.time AS blocking_time,
p.state AS blocking_state,
p.info AS blocking_query
FROM
information_schema.processlist b
JOIN
information_schema.processlist p
WHERE
b.state = 'Waiting for table metadata lock'
AND p.state <> 'Waiting for table metadata lock'
AND p.info IS NOT NULL
AND p.info NOT LIKE '%information_schema.processlist%';
-- 更详细地查看MDL锁信息(MySQL 8.0+)
SELECT
p.id,
p.user,
p.host,
p.db,
p.command,
p.time,
p.state,
p.info,
m.THREAD_ID,
m.OBJECT_NAME,
m.LOCK_TYPE,
m.LOCK_DURATION,
m.LOCK_STATUS
FROM
performance_schema.metadata_locks m
JOIN
information_schema.processlist p ON m.THREAD_ID = p.id
WHERE
m.OBJECT_SCHEMA = 'mdl_test'
AND m.OBJECT_NAME = 'users';
-- 查找长时间运行的事务
SELECT
trx_id,
trx_state,
trx_started,
trx_mysql_thread_id,
trx_query,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_seconds
FROM
information_schema.innodb_trx
WHERE
TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 10
ORDER BY
duration_seconds DESC;
-- 如果情况紧急且无法详细分析,可以使用以下命令找出并杀死所有持有MDL锁的长时间运行的事务
-- 查找所有相关的阻塞进程
SELECT id, user, host, db, command, time, state, info
FROM information_schema.processlist
WHERE db = 'mdl_test'
AND time > 10 -- 设置合理的时间阈值
AND state != 'Waiting for table metadata lock';
-- 查看当前活跃的事务
SELECT * FROM information_schema.INNODB_TRX;
Performance Schema 和 metadata_locks
如果无法查看 metadata_locks
可能是还没有配置。
检查并启用 Performance Schema
首先,确认 Performance Schema 是否已启用:
SHOW VARIABLES LIKE 'performance_schema';
如果返回值是 OFF,您需要在 MySQL 配置文件(通常是 my.cnf 或 my.ini)中添加以下行,然后重启 MySQL:
performance_schema=ON
检查并启用 metadata_locks 的监控
即使 Performance Schema 已启用,metadata_locks
表也可能没有被设置为收集数据。检查是否启用:
SELECT * FROM performance_schema.setup_instruments
WHERE NAME = 'wait/lock/metadata/sql/mdl';
如果 ENABLED 或 TIMED 列为 'NO',请启用它:
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME = 'wait/lock/metadata/sql/mdl';
还需要确保 metadata_locks 表被启用:
UPDATE performance_schema.setup_consumers
SET ENABLED = 'YES'
WHERE NAME = 'global_instrumentation';
UPDATE performance_schema.setup_consumers
SET ENABLED = 'YES'
WHERE NAME LIKE '%locks%';