功能概述
TDSQL Boundless 提供非阻塞 DDL(Non-blocking DDL)功能,用于解决 DDL 操作在等待 MDL 排他锁期间阻塞目标表上所有新事务,导致业务连接堆积甚至系统崩溃的问题。
开启非阻塞 DDL 后,当 DDL 无法立即获取 MDL 排他锁时,系统将暂时释放锁请求,允许新事务正常进入目标表执行,然后以超时重试的方式多次尝试获取锁,直至 DDL 最终完成。
背景信息
DDL 等待 MDL 锁引发的连锁阻塞
在 TDSQL Boundless 中,DDL 操作(如
ALTER TABLE)需要获取目标表的 MDL 排他锁(MDL-X)。当目标表上存在未提交的长事务或大查询时,这些会话持有该表的 MDL 共享读锁,DDL 将进入等待状态。在默认机制下,MDL 排他锁具有最高优先级。DDL 一旦进入等待队列,其后所有对该表的新事务(包括普通的
SELECT、INSERT、UPDATE)都将被阻塞——它们既无法获取 MDL 共享锁(因为等待队列中有优先级更高的排他锁请求),也无法绕过排他锁请求直接执行。这种连锁阻塞会带来严重后果:
1. 业务连接快速堆积,数据库连接池耗尽。
2. 应用层出现大量超时错误,服务不可用。
3. 严重时可能导致整个业务系统崩溃。
非阻塞 DDL 的解决思路
非阻塞 DDL 改变了 MDL 排他锁的获取策略:
默认行为: DDL 提交排他锁请求后持续等待,期间阻塞所有新事务。
非阻塞行为: DDL 在短时间内尝试获取排他锁,如果获取失败,则主动放弃本次锁请求,让新事务正常通过。等待一段间隔后再次尝试,如此循环直至获取成功或达到最大重试次数。
这种"尝试 - 退让 - 重试"的机制确保了即使 DDL 无法立即完成,业务流量也不会被持续阻塞。
与抢占式 DDL 的区别
维度 | 抢占式 DDL | 非阻塞 DDL |
解决的问题 | DDL 因长事务阻塞而执行失败 | DDL 等待期间阻塞所有新事务 |
处理方式 | 超时后终止阻塞 DDL 的会话 | 超时后 DDL 主动退让,放行新事务 |
对已有会话的影响 | 持锁会话会被强制终止 | 不影响任何已有会话 |
DDL 执行结果 | DDL 一定成功 | DDL 可能因重试次数耗尽而失败 |
适用场景 | 可接受中断长事务,要求 DDL 必须成功 | 不可中断业务会话,要求业务零感知 |
两个功能可以根据业务需要单独使用,也可以配合使用。
注意事项
DDL 失败概率增大: 开启非阻塞 DDL 后,DDL 的锁优先级降低。如果目标表上持续有活跃事务,DDL 可能在所有重试次数耗尽后仍无法获取锁,最终执行失败。
DDL 执行时间延长: 由于采用重试机制,DDL 的总执行时间 = 实际 DDL 耗时 + 重试等待时间。在锁冲突严重的场景下,DDL 完成时间可能明显增加。
参数说明
通过以下参数控制非阻塞 DDL 的行为,可在执行 DDL 前动态设置。
参数 | 级别 | 说明 | 取值范围 | 默认值 |
tdsql_ddl_block_mode | Session | 非阻塞 DDL 功能开关,设置为 nonblock 时启用非阻塞 DDL 逻辑。 | preemptive nonblock default | preemptive |
tdsql_ddl_recovery_block_mode | Global | 控制恢复线程的非阻塞 DDL 行为,设置为 nonblock 时生效;仅 GLOBAL 级别设置生效,Session 级别设置无效 | preemptive nonblock default | preemptive |
tdsql_ddl_nonblock_lock_wait_timeout | Session | 单次尝试获取 MDL-X 锁的超时时间。超过该时间未获取到锁,DDL 将主动放弃本次请求,允许新事务通过。单位:秒 | 1 ~ 102410241024 | 1 |
tdsql_ddl_nonblock_retry_interval | Session | 两次锁获取尝试之间的等待间隔。单位:秒 | 0 ~ 60 | 4 |
tdsql_ddl_nonblock_retry_times | Session | 获取 MDL-X 锁的最大重试次数。超过该次数后 DDL 将执行失败。 | 0 ~ ULONG_MAX | 10 |
参数之间的关系:
DDL 的最大等待总时长约为:
总时长 ≈ tdsql_ddl_nonblock_retry_times × (tdsql_ddl_nonblock_lock_wait_timeout + tdsql_ddl_nonblock_retry_interval)
使用方法
1. 开启非阻塞 DDL。
SET tdsql_ddl_block_mode = 'nonblock';
2. (可选)配置重试参数。
例如,设置单次锁等待2秒,重试间隔5秒,最多重试20次:
SET tdsql_ddl_nonblock_lock_wait_timeout = 2;SET tdsql_ddl_nonblock_retry_interval = 5;SET tdsql_ddl_nonblock_retry_times = 20;
3. (可选)配置恢复线程非阻塞 DDL。
SET GLOBAL tdsql_ddl_recovery_block_mode = 'nonblock';
4. 执行 DDL 操作。
ALTER TABLE orders ADD INDEX idx_status (status);
DDL 执行过程中,如果遇到 MDL 锁冲突,系统将按照配置的参数进行"尝试 - 退让 - 重试",业务流量不受影响。
使用示例
下面将通过一个示例,演示非阻塞 DDL 开启前后对业务 TPS 的影响差异。
准备工作
CREATE TABLE sbtest1 (id INT PRIMARY KEY,k INT NOT NULL DEFAULT 0,c CHAR(120) NOT NULL DEFAULT '',pad CHAR(60) NOT NULL DEFAULT '');-- 插入 100 万行测试数据(可通过 SysBench 等工具生成)
使用 SysBench 模拟持续业务负载:
sysbench oltp_read_write \\--mysql-host="连接地址" \\--mysql-port=3306 \\--mysql-user="用户名" \\--mysql-password="密码" \\--mysql-db="test" \\--tables=1 \\--table-size=1000000 \\--threads=8 \\--time=600 \\--report-interval=1 \\run
模拟锁冲突
会话1(模拟未提交的长事务):
BEGIN;SELECT * FROM sbtest1 LIMIT 1;-- 不提交,持有 sbtest1 的 MDL 共享读锁
场景一:关闭非阻塞 DDL(默认行为)
会话2(执行 DDL):
ALTER TABLE sbtest1 ADD COLUMN d INT;-- DDL 进入 MDL 锁等待队列-- 此时所有新事务也被阻塞 → 业务 TPS 跌零
表现: 从 DDL 提交的瞬间开始,SysBench 的 TPS 持续跌零,直到长事务结束或 DDL 超时。业务完全中断。
场景二:开启非阻塞 DDL
会话2(开启非阻塞 DDL 后执行):
SET tdsql_nonblock_ddl_mode = ON;SET tdsql_nonblock_ddl_lock_wait_timeout = 1;SET tdsql_nonblock_ddl_retry_interval = 5;SET tdsql_nonblock_ddl_retry_times = 50;ALTER TABLE sbtest1 ADD COLUMN d INT;-- DDL 尝试获取锁 → 1 秒后放弃 → 放行新事务 → 等待 5 秒 → 再次尝试...-- 当会话 1 的事务结束后,DDL 在下一次重试时获取锁成功-- Query OK, 0 rows affected (23.15 sec)
表现:Sysbench 的 TPS 出现周期性的短暂下降(每次 DDL 尝试获取锁时持续约1秒),但不会跌零。业务整体保持可用。
结果对比
场景 | TPS 表现 | 业务影响 |
关闭非阻塞 DDL | TPS 持续跌零,直到锁释放 | 严重,业务完全中断 |
开启非阻塞 DDL | TPS 周期性短暂下降,不跌零 | 较小,业务保持可用 |
最佳实践
场景一:业务高峰期紧急加索引
业务高峰期需要紧急为慢查询添加索引,但表上有大量活跃事务:
-- 开启非阻塞 DDL,避免影响在线业务SET tdsql_ddl_block_mode = 'nonblock';SET tdsql_ddl_nonblock_lock_wait_timeout = 1;SET tdsql_ddl_nonblock_retry_interval = 3;SET tdsql_ddl_nonblock_retry_times = 100;ALTER TABLE orders ADD INDEX idx_create_time (create_time);
场景二:配合抢占式 DDL 使用
对于关键的表结构变更,可以先尝试非阻塞 DDL,如果长时间无法完成,再启用抢占式 DDL 强制执行:
-- 第一次尝试:非阻塞模式,不影响业务SET tdsql_ddl_block_mode = 'nonblock';SET tdsql_ddl_nonblock_retry_times = 10;ALTER TABLE orders ADD COLUMN remark VARCHAR(255);-- 如果重试耗尽仍失败...-- 第二次尝试:抢占模式,强制完成SET tdsql_ddl_block_mode = 'preemptive';SET tdsql_ddl_preempt_after_wait_seconds = 5;ALTER TABLE orders ADD COLUMN remark VARCHAR(255);
总结:
1. 非阻塞 DDL 核心通过 tdsql_ddl_block_mode(普通线程)/ tdsql_ddl_recovery_block_mode(恢复线程)设置为 nonblock 启用,配套参数控制重试策略;
2. 非阻塞 DDL 采用「尝试 - 退让 - 重试」逻辑,避免阻塞业务事务,适合高并发、对可用性要求高的场景;
3. 可结合抢占式 DDL 使用,优先通过非阻塞模式保障业务,必要时切换抢占模式强制完成 DDL。
相关文档
抢占式 DDL