通过 sys.schema_table_lock_waits
视图诊断元数据锁(MDL)问题
大家好,今天我们来深入探讨如何利用 sys.schema_table_lock_waits
视图来诊断 MySQL 中的元数据锁(MDL)问题。 MDL 是 MySQL 用来保护数据库对象元数据一致性的关键机制,但当 MDL 竞争激烈时,会导致查询阻塞,甚至整个数据库hang住。 理解如何有效地利用 sys.schema_table_lock_waits
可以帮助我们快速定位并解决这些问题。
1. 什么是元数据锁(MDL)?
MDL 是一种轻量级的锁,用于协调对数据库对象(例如表、视图、存储过程等)元数据的并发访问。 当一个会话需要访问一个数据库对象时,它会尝试获取相应的 MDL。 不同类型的操作需要不同类型的 MDL,例如 MDL_SHARED_READ
用于读取数据,MDL_EXCLUSIVE
用于修改表结构。
MDL 的目的是防止在执行 DDL(数据定义语言,如 ALTER TABLE
)操作时,DML(数据操作语言,如 SELECT
, INSERT
, UPDATE
, DELETE
)操作修改了相同的元数据,从而导致数据不一致或崩溃。
2. sys.schema_table_lock_waits
视图简介
sys.schema_table_lock_waits
视图是 MySQL 5.7 及更高版本中 sys
数据库的一部分。 它提供了一个实时的快照,显示了当前正在等待 MDL 的会话的信息。 这个视图对于识别哪些会话正在等待 MDL,以及哪些会话正在持有阻止其他会话获取 MDL 的锁至关重要。
3. sys.schema_table_lock_waits
视图的结构
该视图包含以下关键列:
列名 | 数据类型 | 描述 |
---|---|---|
object_schema |
varchar |
正在等待锁的对象的数据库名称。 |
object_name |
varchar |
正在等待锁的对象的表名称。 |
waiting_pid |
bigint |
正在等待锁的进程 ID (PID)。 |
waiting_query |
longtext |
正在等待锁的会话正在执行的查询语句。 |
waiting_lock_type |
varchar |
正在等待的锁的类型 (例如, MDL_SHARED_READ , MDL_EXCLUSIVE )。 |
waiting_lock_duration |
varchar |
正在等待的锁的持续时间 (例如, TRANSACTION , STATEMENT )。 |
blocking_pid |
bigint |
持有阻止 waiting_pid 获取锁的锁的进程 ID (PID)。 |
blocking_query |
longtext |
持有阻止 waiting_pid 获取锁的锁的会话正在执行的查询语句。 |
blocking_lock_type |
varchar |
持有的阻止 waiting_pid 获取锁的锁的类型。 |
blocking_lock_duration |
varchar |
持有的阻止 waiting_pid 获取锁的锁的持续时间。 |
4. 使用 sys.schema_table_lock_waits
诊断 MDL 问题
现在,我们通过一些实际的例子来说明如何使用 sys.schema_table_lock_waits
视图来诊断 MDL 问题。
场景 1: 长时间运行的 DDL 阻塞了 DML
假设我们正在执行一个 ALTER TABLE
操作,该操作需要很长时间才能完成。 在此期间,其他会话尝试读取或写入相同的表,但它们会被阻塞,因为 ALTER TABLE
持有一个排他 MDL 锁。
首先,我们创建一个测试表:
CREATE TABLE test_mdl (
id INT PRIMARY KEY,
value VARCHAR(255)
);
INSERT INTO test_mdl (id, value) VALUES (1, 'initial value');
然后,在一个会话中,我们执行一个长时间运行的 ALTER TABLE
操作(这里我们用 SLEEP()
函数模拟):
-- 会话 1
ALTER TABLE test_mdl ADD COLUMN new_column INT AFTER value;
SELECT SLEEP(60); -- 模拟长时间运行的 DDL
ALTER TABLE test_mdl DROP COLUMN new_column;
在另一个会话中,我们尝试读取该表:
-- 会话 2
SELECT * FROM test_mdl; -- 此查询会被阻塞
现在,我们可以使用 sys.schema_table_lock_waits
视图来查看等待锁的信息:
SELECT
object_schema,
object_name,
waiting_pid,
waiting_query,
waiting_lock_type,
waiting_lock_duration,
blocking_pid,
blocking_query,
blocking_lock_type,
blocking_lock_duration
FROM
sys.schema_table_lock_waits;
输出结果可能如下所示:
object_schema | object_name | waiting_pid | waiting_query | waiting_lock_type | waiting_lock_duration | blocking_pid | blocking_query | blocking_lock_type | blocking_lock_duration |
---|---|---|---|---|---|---|---|---|---|
test | test_mdl | 123 | SELECT * FROM test_mdl | MDL_SHARED_READ | TRANSACTION | 456 | ALTER TABLE test_mdl ADD COLUMN new_column INT AFTER value; SELECT SLEEP(60); ALTER TABLE test_mdl DROP COLUMN new_column; | MDL_EXCLUSIVE | TRANSACTION |
从这个结果中,我们可以看到:
waiting_pid
为 123 的会话正在等待MDL_SHARED_READ
锁,它正在执行SELECT * FROM test_mdl
。blocking_pid
为 456 的会话正在持有MDL_EXCLUSIVE
锁,它正在执行ALTER TABLE
操作。
通过这个信息,我们可以确定长时间运行的 ALTER TABLE
操作是导致 SELECT
语句被阻塞的原因。 解决办法是尽快完成 ALTER TABLE
操作,或者考虑使用在线 DDL 工具(例如 pt-online-schema-change
)来最小化对其他查询的影响。
场景 2: 事务未提交导致 MDL 锁持有
另一种常见的 MDL 问题是事务未提交导致 MDL 锁被长时间持有。 例如,一个会话启动了一个事务,执行了一些 DML 操作,但忘记提交或回滚事务。 这会导致该会话持有 MDL 锁,阻止其他会话访问相同的表。
在一个会话中,我们启动一个事务并更新表:
-- 会话 1
START TRANSACTION;
UPDATE test_mdl SET value = 'updated value' WHERE id = 1;
-- 注意:这里没有提交或回滚事务
在另一个会话中,我们尝试修改表结构:
-- 会话 2
ALTER TABLE test_mdl ADD INDEX idx_value (value); -- 此查询会被阻塞
再次使用 sys.schema_table_lock_waits
视图来查看等待锁的信息:
SELECT
object_schema,
object_name,
waiting_pid,
waiting_query,
waiting_lock_type,
waiting_lock_duration,
blocking_pid,
blocking_query,
blocking_lock_type,
blocking_lock_duration
FROM
sys.schema_table_lock_waits;
输出结果可能如下所示:
object_schema | object_name | waiting_pid | waiting_query | waiting_lock_type | waiting_lock_duration | blocking_pid | blocking_query | blocking_lock_type | blocking_lock_duration |
---|---|---|---|---|---|---|---|---|---|
test | test_mdl | 789 | ALTER TABLE test_mdl ADD INDEX idx_value (value) | MDL_EXCLUSIVE | TRANSACTION | 101 | UPDATE test_mdl SET value = ‘updated value’ WHERE id = 1 | MDL_EXCLUSIVE | TRANSACTION |
从这个结果中,我们可以看到:
waiting_pid
为 789 的会话正在等待MDL_EXCLUSIVE
锁,它正在执行ALTER TABLE
操作。blocking_pid
为 101 的会话正在持有MDL_EXCLUSIVE
锁,它正在执行UPDATE
操作,并且事务未提交。
解决办法是找到 blocking_pid
对应的会话,并强制终止该会话或提交/回滚事务。 可以使用 KILL
命令终止会话:
KILL 101; -- 终止会话 101
场景 3: 大量并发查询导致 MDL 竞争
在高并发的场景下,即使查询本身运行很快,大量的并发查询也可能导致 MDL 竞争,从而降低整体性能。 例如,大量的 SELECT
语句同时访问同一个表,每个 SELECT
语句都需要获取 MDL_SHARED_READ
锁。
在这种情况下,sys.schema_table_lock_waits
视图可能显示大量的会话正在等待 MDL_SHARED_READ
锁。 但是,由于每个锁的持续时间都很短,所以很难确定哪个会话是主要的阻塞者。
解决办法是优化查询,减少对元数据的访问。 例如,可以考虑使用查询缓存,避免重复解析查询语句。 还可以优化表结构,减少锁的粒度。
5. 其他有用的诊断工具和技巧
除了 sys.schema_table_lock_waits
视图之外,还有其他一些工具和技巧可以帮助我们诊断 MDL 问题:
SHOW PROCESSLIST
: 显示当前 MySQL 服务器上正在执行的线程的信息,包括线程的状态、正在执行的查询等。 可以用来查找长时间运行的查询或未提交的事务。performance_schema
: 提供更详细的性能监控信息,包括锁的等待时间和持有时间。 可以使用performance_schema.metadata_locks
表来查看 MDL 的详细信息。INFORMATION_SCHEMA.PROCESSLIST
: 类似于SHOW PROCESSLIST
,但是可以通过 SQL 查询来获取信息。- 慢查询日志: 记录执行时间超过指定阈值的查询语句。 可以用来查找需要优化的查询。
6. 预防 MDL 问题
预防胜于治疗。 以下是一些可以帮助我们预防 MDL 问题的最佳实践:
- 保持事务简短: 尽量避免长时间运行的事务,尽快提交或回滚事务。
- 避免在生产环境中执行 DDL 操作: 尽量在非高峰期执行 DDL 操作,或者使用在线 DDL 工具。
- 优化查询: 优化查询语句,减少对元数据的访问。
- 使用查询缓存: 使用查询缓存,避免重复解析查询语句。
- 监控 MDL 锁: 定期监控
sys.schema_table_lock_waits
视图,及时发现并解决 MDL 问题。 - 合理设计表结构: 合理设计表结构,减少锁的粒度。例如,可以将大表拆分成小表,或者使用分区表。
7. 一个更复杂的示例
假设我们遇到一个非常棘手的 MDL 问题,sys.schema_table_lock_waits
显示多个会话相互阻塞,形成一个死锁。
-- 会话 A
START TRANSACTION;
UPDATE table1 SET col1 = 1 WHERE id = 1;
-- 等待会话 B 释放对 table2 的锁
-- 会话 B
START TRANSACTION;
UPDATE table2 SET col2 = 2 WHERE id = 2;
-- 等待会话 A 释放对 table1 的锁
在这种情况下,sys.schema_table_lock_waits
可能会显示:
object_schema | object_name | waiting_pid | waiting_query | waiting_lock_type | waiting_lock_duration | blocking_pid | blocking_query | blocking_lock_type | blocking_lock_duration |
---|---|---|---|---|---|---|---|---|---|
test | table2 | PID_B | UPDATE table2 … | MDL_EXCLUSIVE | TRANSACTION | PID_A | UPDATE table1 … | MDL_EXCLUSIVE | TRANSACTION |
test | table1 | PID_A | UPDATE table1 … | MDL_EXCLUSIVE | TRANSACTION | PID_B | UPDATE table2 … | MDL_EXCLUSIVE | TRANSACTION |
解决死锁的常见方法是终止其中一个会话。 但是,在生产环境中,盲目地终止会话可能会导致数据丢失或不一致。 因此,我们需要仔细分析死锁的原因,并采取适当的措施。
在这种情况下,我们可以考虑以下解决方案:
- 应用程序层面修改: 重新设计应用程序逻辑,避免跨多个表的事务,或者使用更细粒度的锁。
- 设置锁等待超时: 设置
innodb_lock_wait_timeout
参数,当锁等待时间超过指定阈值时,自动回滚事务。 这可以防止死锁长时间阻塞数据库。 - 调整事务隔离级别: 如果可能,可以考虑使用较低的事务隔离级别,例如
READ COMMITTED
,以减少锁的竞争。
关键点的回顾
通过 sys.schema_table_lock_waits
视图,我们可以诊断由长时间运行的 DDL 操作、未提交的事务或高并发查询导致的 MDL 问题。 结合其他诊断工具,并采取预防措施,可以有效地避免和解决 MDL 问题,提高数据库的性能和稳定性。