Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > mysql ddl 超时

mysql ddl 超时场景模拟与排查实现

作者:猫咪很可爱

本文主要介绍了mysql ddl 超时场景模拟与排查实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

前言

本文通过一个可复现的实验,演示 MySQL 中一个常见却容易被忽视的阻塞场景:当某个会话开启事务并写入数据后一直不提交,该事务会持有表的 MDL 共享锁,导致其他会话执行 DDL 或 TRUNCATE 时被阻塞,最终触发 lock_wait_timeout 超时。实验按「建表 → 插入示例数据 → 模拟未提交事务 → 触发 DDL 超时 → 排查阻塞链」五个步骤展开,帮助你直观理解 MDL 锁的机制与排查方法。

建表语句

CREATE DATABASE IF NOT EXISTS orders_db;

USE orders_db;

CREATE TABLE IF NOT EXISTS orders (
    id           BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单ID',
    order_no     VARCHAR(32)     NOT NULL COMMENT '订单编号',
    user_id      BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
    amount       DECIMAL(12,2)   NOT NULL DEFAULT 0.00 COMMENT '订单金额',
    status       TINYINT         NOT NULL DEFAULT 0 COMMENT '0-待支付 1-已支付 2-已发货 3-已完成 4-已取消',
    created_at   DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at   DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (id),
    UNIQUE KEY uk_order_no (order_no),
    KEY idx_user_created (user_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单表';

插入示例数据

-- 单条插入
INSERT INTO orders_db.orders (order_no, user_id, amount, status, created_at)
VALUES ('ORD202610180001', 10001, 199.00, 1, '2026-10-18 01:30:00');

INSERT INTO orders_db.orders (order_no, user_id, amount, status, created_at)
VALUES ('ORD202610180002', 10002, 359.50, 0, '2026-10-18 01:35:00');

INSERT INTO orders_db.orders (order_no, user_id, amount, status, created_at)
VALUES ('ORD202610180003', 10003, 99.90, 2, '2026-10-18 01:40:00');


-- 批量插入
INSERT INTO orders_db.orders (order_no, user_id, amount, status, created_at)
VALUES
    ('ORD202610180010', 10010, 120.00, 0, '2026-10-18 01:50:00'),
    ('ORD202610180011', 10011, 230.00, 1, '2026-10-18 01:51:00'),
    ('ORD202610180012', 10012, 88.88, 2, '2026-10-18 01:52:00'),
    ('ORD202610180013', 10013, 560.00, 3, '2026-10-18 01:53:00'),
    ('ORD202610180014', 10014, 79.90, 0, '2026-10-18 01:54:00');

模拟“不断写入但不提交”

在会话 A中执行

USE orders_db;

SET autocommit = 0;

BEGIN;

INSERT INTO orders (order_no, user_id, amount, status, created_at)
VALUES ('ORD_UNCOMMIT_001', 90001, 1.00, 0, NOW());

INSERT INTO orders (order_no, user_id, amount, status, created_at)
VALUES ('ORD_UNCOMMIT_002', 90002, 2.00, 0, NOW());

-- 故意不执行 COMMIT / ROLLBACK

这个事务会一直持有 orders 表的 MDL 锁。

让 DDL / TRUNCATE 超时

在会话 B中执行

USE orders_db;

SET SESSION lock_wait_timeout = 5;

TRUNCATE TABLE orders;

大约 5 秒后会报错:

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

因为 TRUNCATE TABLE 是 DDL,需要获取表的 MDL 排他锁;会话 A 的未提交事务已经持有该表的 MDL 共享锁,所以会话 B 的 DDL 会被阻塞,直到 lock_wait_timeout 超时。

下面用一张兼容性矩阵示意 MDL 共享锁与排他锁的兼容关系,并标注本文实验中会话 A 持有共享锁、会话 B 请求排他锁导致阻塞的路径:

请求锁 \\ 希望获取的锁

已持有锁 \\

MDL 共享锁(SHARED)

已持有锁 \\

MDL 排他锁(EXCLUSIVE)

MDL 共享锁(SHARED)兼容(可共存)互斥(等待)
MDL 排他锁(EXCLUSIVE)互斥(等待)互斥(等待)

本文实验中的阻塞路径:会话 A 开启事务并写入数据后不提交,持有 orders 表的 MDL 共享锁;会话 B 执行 TRUNCATE TABLE orders,需要获取该表的 MDL 排他锁。由于共享锁与排他锁互斥,会话 B 的请求只能等待,直到 lock_wait_timeout 超时并报错 ERROR 1205。这条路径正是上表中「共享锁已持有、排他锁请求」一格所对应的互斥场景。

排查用 SQL

如果后续想观察阻塞链,可以用:

SELECT * FROM information_schema.INNODB_TRX;

SELECT * FROM performance_schema.metadata_locks
WHERE OBJECT_SCHEMA = 'orders_db' AND OBJECT_NAME = 'orders';

SHOW FULL PROCESSLIST;

下表对比了 MDL 共享锁与排他锁在兼容性、持有场景和阻塞对象上的差异:

锁类型兼容性典型持有场景阻塞对象
MDL 共享锁(SHARED)与共享锁兼容,与排他锁互斥DML 操作(SELECT、INSERT、UPDATE、DELETE)以及未提交事务DDL、TRUNCATE、ALTER 等需要排他锁的操作
MDL 排他锁(EXCLUSIVE)与共享锁、排他锁均互斥DDL、TRUNCATE、ALTER TABLE 等结构变更操作所有需要访问该表的 DML 与 DDL 操作

简要说明:MDL 锁的兼容性决定了阻塞方向。共享锁之间可以共存,因此多个会话可以同时读写同一张表;但共享锁与排他锁互斥,一旦有事务持有共享锁且不提交,后续的 DDL 或 TRUNCATE 就会一直等待,直到 lock_wait_timeout 超时。这也是本文实验中会话 B 的 TRUNCATE 被阻塞的根本原因。

注意事项

  1. 只在测试库执行,不要在生产库跑。
  2. 模拟结束后,在会话 A 执行 COMMIT; 或 ROLLBACK;,否则 orders 表上的 MDL 锁会一直存在。

如何避免 MDL 锁阻塞

结合上面的实验,可以从以下三个方面入手,尽量避免 MDL 锁阻塞带来的 DDL 或 TRUNCATE 超时问题:

  1. 事务中避免长时间持有锁,及时提交或回滚。未提交事务是 MDL 共享锁最常见的持有来源。业务代码中应尽量缩短事务执行时间,避免在事务内做耗时操作(如远程调用、批量计算),并在操作完成后立即执行 COMMIT; 或 ROLLBACK;,及时释放锁。
  2. 执行 DDL 前检查 metadata_locks,确认无锁持有。在执行 ALTER TABLE、TRUNCATE 等结构变更前,先查询 performance_schema.metadata_locks,确认目标表上没有其他会话持有的 MDL 共享锁或排他锁,再发起 DDL,避免盲目等待。
  3. 设置合理的 lock_wait_timeout 并监控长时间未提交事务。根据业务容忍度设置合适的 lock_wait_timeout,避免 DDL 无限期阻塞;同时结合 information_schema.INNODB_TRX 定期监控长时间未提交的事务,及时发现并处理潜在的锁持有者。

总结

本文通过一个可复现的实验,完整演示了 MDL 锁阻塞 DDL 的典型场景:未提交事务持有表的 MDL 共享锁,导致其他会话执行 TRUNCATE 或 DDL 时被阻塞,最终触发 lock_wait_timeout 超时。核心机制在于 MDL 锁的兼容性——共享锁与排他锁互斥,只要事务不提交,锁就不会释放。实验的关键步骤包括:建表、插入示例数据、在会话 A 中开启事务并写入但不提交、在会话 B 中设置较短的 lock_wait_timeout 后执行 TRUNCATE、最后通过 INNODB_TRX 和 metadata_locks 等视图排查阻塞链。排查时建议优先查看 performance_schema.metadata_locks 确认锁的持有者,再结合 INNODB_TRX 定位未提交事务,从而快速找到阻塞源头。

参考资料

到此这篇关于mysql ddl 超时场景模拟与排查实现的文章就介绍到这了,更多相关mysql ddl 超时内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文