十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TiDB Multi-Schema Change(多模式变更)设计与实现解析:单条 ALTER TABLE 原子执行多项 DDL

TiDB Multi-Schema Change(多模式变更)设计与实现解析:单条 ALTER TABLE 原子执行多项 DDL TiDB Multi-Schema Change多模式变更设计与实现解析单条 ALTER TABLE 原子执行多项 DDL【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidb一条ALTER TABLE里同时加列、改列类型、加索引、改注释——这既是 MySQL 对 SQL 标准的扩展能力也是 TiDB 长期缺失、从 MySQL 迁移用户频繁碰壁的功能点。TiDB 在 2022 年 4 月提交的设计方案 docs/design/2022-04-15-multi-schema-change.md 中给出了完整解决思路引入SubJob子任务模型把一条语句拆解为多个子任务装入同一个 DDL Job再借助可回滚/不可回滚两阶段发布保证原子性。阅读本文后你将掌握 Multi-Schema Change 的Job/SubJob数据结构、状态机与回滚语义、与在线 DDL 架构的衔接方式以及 TiDB 与 MySQL 在该能力上的兼容性差异。一、背景为什么需要 Multi-Schema ChangeMySQL 提供了一种超出 SQL 标准的能力用户可以在一条ALTER TABLE语句中原子地完成多项模式变更schema change包括列与索引的ADD、ALTER、DROP、CHANGE以及表选项的修改。例如CREATE TABLE t (a INT, c INT); ALTER TABLE t ADD COLUMN b INT, MODIFY COLUMN c CHAR(5), ADD INDEX idx(a), ADD PRIMARY KEY (c), COMMENT comments for t;而在设计文档提出的时间点TiDB 每条 SQL 语句仅支持一项模式变更只在少数特殊场景下有限地支持多项变更。对从 MySQL 系数据库迁移的用户来说必须手动把一条 Multi-Schema Change DDL 改写成多条单变更 DDL对于依赖 Flyway 这类 ORM 框架自动生成 SQL 的场景改写工作繁琐且脚本难以维护。因此文档将缺少该能力定义为阻碍用户使用 TiDB 的阻塞性问题blocking issue。目标与非目标目标支持 MySQL 兼容、且使用广泛的 Multi-Schema Change包括ADD/DROP COLUMN、ADD/DROP INDEX、MODIFY COLUMN、RENAME COLUMN等。明确不做的事Non-Goals不支持 TiDB 特有的多变更组合如ADD TIFLASH REPLICA、ADD PARTITION、ALTER PARTITION等在 executor.go 中对ActionSetTiFlashReplica等类型直接返回ErrRunMultiSchemaChanges可验证这一点不解决 DDL 与 DML 并发执行时的 schema is changed 错误不追求与 MySQL 100% 行为一致MySQL 会对变更执行顺序做重排有时反直觉不直接优化 DDL 性能虽然为长任务提速预留了空间见未来工作。二、总体设计在线 DDL 架构之上的 SubJob 模型Multi-Schema Change 的实现建立在 TiDB 的在线 DDL 架构之上参见设计文档 docs/design/2018-10-08-online-DDL.md。其核心思想是不新增多个独立的 DDL Job而是把多项变更打包进同一个 Job。设计文档据此明确了两个概念Job一条 DDL 语句在 TiDB 内部的通用表示SubJob一个 DDL 模式变更的表示。一个 Job 可以包含零个当 Multi-Schema Change 不适用时或多个 SubJob。数据结构Job / SubJob / MultiSchemaInfo设计文档给出的核心数据结构如下文档中简写正式实现在 pkg/meta/model/job.go// Job represents a DDL action. type Job struct { Type ActionType json:type State JobState json:state // ... MultiSchemaInfo *MultiSchemaInfo json:multi_schema_info } // MultiSchemaInfo contains information for multi-schema change. type MultiSchemaInfo struct { // ... SubJobs []SubJob json:sub_jobs } // SubJob represents one schema change in a multi-schema change DDL. type SubJob struct { Type ActionType json:type State JobState json:state // ... }字段ActionType表示 DDL 类型例如ADD COLUMN映射为ActionAddColumnMODIFY COLUMN映射为ActionModifyColumn。Multi-Schema Change 的 DDL Job 类型固定为ActionMultiSchemaChange。在当前 worker 模型中有专门的代码路径onMultiSchemaChange()负责执行这类 Job只有 Multi-Schema Change Job 才允许携带 SubJob。举例对于如下 DDLALTER TABLE t ADD COLUMN b INT, MODIFY COLUMN a CHAR(10);可以被建模为job : Job { Type: ActionMultiSchemaChange, MultiSchemaInfo: MultiSchemaInfo { SubJobs: []*SubJob { SubJob { Type: ActionAddColumn, Args: ... }, SubJob { Type: ActionModifyColumn, Args: ... }, } } }这样多个变更就被打包进一个 Job和其他 Job 一样进入持久化的 DDL Job 队列等待合适的 worker 拾取并处理。落地到真实代码的结构上面是设计文档的示意代码。在当前仓库中真实结构已演化为Job、SubJob、MultiSchemaInfo三个正式类型均定义在 pkg/meta/model/job.goJobjob.go含Type、State、SchemaState、MultiSchemaInfo等字段注释明确写着MultiSchemaInfo keeps some warning now for multi schema changeSubJobjob.go注释与设计文档一致A SubJob is a representation of one DDL schema change. A Job may contain zero (when multi-schema change is not applicable) or more SubJobs.并比设计文档多出Revertible、RowCount、SchemaVer、NeedReorg、ReorgTp、ReorgStage等运行时字段——这些字段承担了两阶段发布、行数统计与 reorg 状态记录职责MultiSchemaInfojob.go除持久化的SubJobs、Revertible、Seq、SkipVersion外还有仅在内存中使用的AddColumns、DropColumns、ModifyColumns、AddIndexes、DropIndexes、AlterIndexes、AddForeignKeys、RelativeColumns、PositionColumns等集合供校验与冲突检测使用。三、Job / SubJob 的执行与状态机状态迁移设计文档给出Job与SubJob共有的State字段及全部可能的迁移路径┌----- Done -------------------┐ | | None - Running - Rollingback - RollbackDone - Synced | | └----- Cancelling - Cancelled ---┘这些状态可以被分为四类状态正常Normal异常Abnormal未完成UncompletedNone、RunningRollingback、Cancelling已完成CompletedDoneRollbackDone、Cancelled真实枚举值定义在 pkg/meta/model/job.goJobStateRunning、JobStateRollingback、JobStateRollbackDone、JobStateDone、JobStateCancelled、JobStateSynced、JobStateCancelling、JobStateQueueing、JobStatePausing。SubJob 上的IsNormal()、IsFinished()辅助方法job.go即按此分类判断子任务属于正常推进还是已终结。子任务选择顺序由于一个 Job 由一个 DDL worker 单线程执行SubJob 也就在单线程内逐个执行。设计文档给出的选择原则是正常状态下按升序选择第一个未完成的 SubJob即序号小的先执行异常状态下按降序选择第一个未完成的 SubJob即序号大的先执行先回滚后执行的那几个变更。当任意一个 SubJob 进入异常状态时父 Job 及其余所有 SubJob 都要切换到异常状态。这一点在 multi_schema_change.go 的handleRevertibleException中得到印证一旦某个 SubJob 非正常父 Job 置为Rollingback正在Running的子任务置为Cancelling处于None/Queueing的子任务置为Cancelled。四、Schema 对象状态管理与两阶段发布原子性核心为什么需要延迟发布为保证 Multi-Schema Change 的原子执行必须精细管理正在变更的 schema 对象的状态。以文档的示例说明ALTER TABLE t ADD COLUMN b INT, MODIFY COLUMN a CHAR(10);如果第二个 SubJobMODIFY COLUMN a CHAR(10)因某种原因失败例如某行数据超过十个字符第一个 SubJob 必须能够回滚其变更撤销已添加的列b。这个要求意味着不能在一个 SubJob 完成时立即发布其 schema 对象。相反它应当保持在用户不可见的状态等待其余 SubJob 完成只有确认所有 SubJob 都成功后才一次性全部发布。设计文档指出该方式与 2PC 类似只有所有 prewrite 完成commit 才能开始。各变更的可回滚/不可回滚状态表以下是不同 DDL 中可能出现的 schema 状态表Revertible States 指对用户不可见的变更即仍可整体回滚DDLSchema Change可回滚状态Revertible States不可回滚状态Non-revertible StatesAdd ColumnNone, Delete-Only, Write-Only, Write-ReorgPublicAdd IndexNone, Delete-Only, Write-Only, Write-ReorgPublicDrop ColumnPublicWrite-Only, Delete-Only, Delete-Reorg, NoneDrop IndexPublicWrite-Only, Delete-Only, Delete-Reorg, None非 Reorg 的 Modify ColumnPublic元数据变更前Public元数据变更后Reorg 的 Modify ColumnNone, Delete-Only, Write-Only, Write-ReorgPublic借助non-revertible标记推进为达成上述行为设计文档在 SubJob 中引入名为non-revertible的标记。当某个 schema 对象到达最后一个可回滚状态时该标记被置位带此标记的 SubJob 被视为临时完成worker 得以选择下一个 SubJob。当所有 SubJob 都 non-revertible 后所有相关 schema 对象在一个事务内统一切换到下一状态之后各 SubJob 串行执行剩余步骤。在 pkg/meta/model/job.go 中MultiSchemaInfo.Revertible即承担该标记角色NewMultiSchemaInfo()初始化为trueSubJob.Revertible则记录单个子任务的对应状态而 onMultiSchemaChange 中调用job.MarkNonRevertible()表示所有子任务已越过最后可回滚点。两阶段执行的源码印证从 pkg/ddl/multi_schema_change.go 的实现可以看到onMultiSchemaChange明确分为两个阶段执行可回滚阶段job.MultiSchemaInfo.Revertible true逐个挑选尚未Revertible置假且未结束的子任务执行一旦有异常则调用handleRevertibleException让整个 Job 进入回滚。当所有子任务都到达最后可回滚点时代码将剩余 SubJob 统一step一次并写入 schema diff// 只生成 1 个 schema version把这些 sub-job 一次性推到不可回滚状态 for i, sub : range job.MultiSchemaInfo.SubJobs { proxyJob : sub.ToProxyJob(job, i) if schemaVersionGenerated { proxyJob.MultiSchemaInfo.SkipVersion true } proxyJobVer, _, err : w.runOneJobStep(jobCtx, proxyJob) // ... } // 合并成一个 schema diff而不是每个子任务各生成一份 if err metaMut.SetSchemaDiff(model.SchemaDiff{ Version: ver, Type: job.Type, TableID: job.TableID, SchemaID: job.SchemaID, SubActionTypes: actionTypes, }); err ! nil { ... } job.MarkNonRevertible()可以看到跳过额外版本生成与多个子动作汇总为单个SchemaDiff两处细节从源码层面保证了多个 SubJob 的批量推进只对外暴露一次 schema 版本变化这正是一次性发布的落地实现。不可回滚阶段Revertible false逐个串行执行其余不可回滚的子任务最后调用finishMultiSchemaJobmulti_schema_change.go将父 Job 置为JobStateDone。回滚与异常处理反过来如果在所有 SubJob 变为 non-revertible 之前有任何子任务返回错误整个 Job 进入Rollingback状态已执行的 SubJob 置为Cancelling未执行的置为Cancelled。回滚时子任务按逆序逐个撤销对应onMultiSchemaChange开头for i : len(...) - 1; i 0; i--的逆序遍历multi_schema_change.go。最后考虑极端情况错误发生在所有 SubJob 都已 non-revertible 之后。此时错误只有两类可能逻辑错误违反唯一约束、数据超范围等与物理错误网络不可用、存储不可用等。文档指出此时错误必然是物理性质的通常可简单解决如重试这与现有 DDL 实现保持一致——例如DROP COLUMN一旦列进入 Write-Only 状态便无法中止该 Job。源码中handleRollbackExceptionmulti_schema_change.go也印证了物理错误在回滚期间不可恢复、只能持续重试的处理策略。可回滚性相关校验除了状态管理TiDB 还会在提交前做语义检查确保变更组合本身是合法可回滚的checkOperateSameColAndIdxmulti_schema_change.go同一列或同一索引不能被多次操作对应错误如重复操作列名checkMultiSchemaInfomulti_schema_change.go汇总校验重复操作、可见列数量是否越界、外键依赖的索引是否被误删、加列后总列数是否超限checkOperateDropIndexUseByForeignKeymulti_schema_change.go防止在同一语句中删掉仍被外键引用的索引。五、从 SQL 到 SubJob语句拆解与提交链路理解了执行侧的状态机再回看一条 Multi-Schema Change SQL 是如何在提交阶段被识别并拆成 SubJob 的。整条链路分布在 executor 与 jobsubmit 两个模块识别为 Multi-Schema ChangeisMultiSchemaChangespkg/ddl/executor.go判断条件为spec 数量大于 1或者单条 spec 为ADD COLUMNS且新增列数大于 1。此外若开启了行级校验和row level checksum会直接拒绝多模式变更。开启收集模式当len(validSpecs) 1时executor.go 在会话的StmtCtx中挂上一个model.NewMultiSchemaInfo()。随后AlterTable仍按原逻辑逐条调用各变更处理器AddColumn、dropIndex、ModifyColumn等但此时DoDDLJob并不会真正提交执行而是把每个变更产生的 Job 逐一收集进MultiSchemaInfo——这正是注释所描述的 DoDDLJob will collect all jobs into MultiSchemaInfo and skip running them. Then we will run them in multiSchemaChange all at once.。聚合为一个父 Job收集完成后所有子 Job 被折叠为一个类型为ActionMultiSchemaChange的父 Job。appendToSubJobsmulti_schema_change.go按各自类型把JobArgs、RawArgs、SchemaState、NeedReorg等塞入SubJobfillMultiSchemaInfomulti_schema_change.go则根据ADD/DROP COLUMN、ADD/DROP INDEX、RENAME INDEX、MODIFY COLUMN、外键增删等类型填充AddColumns/DropColumns/ModifyColumns/AddIndexes/DropIndexes等元信息不支持的类型会以ErrRunMultiSchemaChanges报错。为提升一次加多个索引场景的效率还存在mergeAddIndexmulti_schema_change.go把多个ADD INDEX子任务合并成一个放到子任务末尾。持久化入队父 Job 进入持久化的 DDL Job 队列。序列化时fillArgsWithSubJobspkg/ddl/jobsubmit/submit.go会为每个 SubJob 单独FillArgs一旦入队成功setJobStateToQueueingsubmit.go会把父 Job 与所有 SubJob 一并置为Queueing。这意味着整个多模式变更是作为一个整体被调度与持久化的天然具备原子与可恢复性。Owner 执行DDL owner 侧通过onMultiSchemaChange()专用路径执行。执行时通过SubJob.ToProxyJobjob.go把每个子任务包装成一个临时 proxy Job交给既有的单任务执行框架runOneJobStep跑完一步再用SubJob.FromProxyJobjob.go把结果状态回收——这样 Multi-Schema Change 得以复用全部既有单 DDL 的执行逻辑包括 reorg、schema 版本推进、错误处理等无需为每种变更重写一套执行引擎。这一设计是理解 Multi-Schema Change 最重要的实现细节之一。六、兼容性分析升级兼容性集群滚动升级期间存在两种情形对应设计文档DDL owner 是新版本 TiDB用户在旧版本TiDB 上执行 Multi-Schema Change 语句时会收到 unsupported multi-schema change 错误DDL owner 是旧版本 TiDB用户在新版本TiDB 上执行 Multi-Schema Change 语句时会收到 invalid ddl job type 错误。两种情况下 Multi-Schema Change 语句都无法执行因此滚动升级期间应避免执行 Multi-Schema Change。MySQL 兼容性差异MySQL 会对部分变更做重排。设计文档给出如下例子CREATE TABLE t (b INT, a INT, INDEX i(b)); ALTER TABLE t DROP COLUMN b, RENAME COLUMN a TO b, ADD INDEX i(b), DROP INDEX i; -- success! SHOW CREATE TABLE t;-------------------------------------------------------------------------------------------------------------------------------------- | Table | Create Table | -------------------------------------------------------------------------------------------------------------------------------------- | t | CREATE TABLE t ( b int DEFAULT NULL, KEY i (b) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci | --------------------------------------------------------------------------------------------------------------------------------------MySQL 的实际执行顺序为DROP INDEX iDROP COLUMN bRENAME a TO bADD INDEX i(b)由于这种重排行为有些反直觉TiDB决定不与 MySQL 完全兼容。TiDB 会基于执行前获取的快照 schema 结构校验各项变更而忽略同一条语句内前面变更产生的影响因此同样的语句在 TiDB 中会报错ALTER TABLE t DROP COLUMN b, RENAME COLUMN a TO b, ADD INDEX i(b), DROP INDEX i; ERROR 1060 (42S21): Duplicate column name b这种基于快照校验的语义也会影响一些常见用法例如ALTER TABLE t ADD COLUMN a, ADD INDEX i1(a);目前不被支持——因为校验时列a尚不存在于快照结构中。不过文档也指出支持这类语句并不困难去掉特定校验即可。七、未来工作设计文档指出该实现可以被用于发展其他能力或优化表级数据重组如ALTER TABLE CONVERT TO CHARSET可以通过把一个 Job 拆成多个 SubJob 来实现多索引添加性能优化通过把多个ADD INDEX子任务合并为一个 SubJob数据只读取一次而非多次这一设想已在 mergeAddIndex 中部分落地EXPLAIN DDL帮助用户理解多模式变更的工作方式展示执行顺序、子任务等信息。八、备选方案与取舍设计文档还记录了几种被否定的备选方案方便读者理解为何选择 SubJob 模型Job group 方案用一组 job 来表示多个变更。问题在于它会把 job group 的内部细节暴露给 DDL Job 队列使维护更困难逐个 case 特殊实现例如历史 PR #15540某些场景虽可支持但不可扩展放弃原子性、使用多个独立 DDL Job实现复杂度最低、工作量小但会让用户看到混乱的中间状态。三者相较SubJob方案在内部细节隔离、可扩展性与原子语义之间取得平衡。九、测试与验证本特性有充分的测试覆盖可作为深入研读的实现佐证pkg/ddl/multi_schema_change_test.go单元级测试覆盖了各类组合与异常路径例如TestMultiSchemaChangeAddColumnsCancelled、TestMultiSchemaChangeDropIndexedColumnsCancelled、TestMultiSchemaChangeRenameColumns、TestMultiSchemaChangeAddIndexesCancelled、TestMultiSchemaChangeModifyColumnsCancelled、TestMultiSchemaChangeMixCancelled、TestMultiSchemaChangeAdminShowDDLJobs、TestMultiSchemaChangeWithExpressionIndex、TestMultiSchemaChangeNoSubJobs、TestMultiSchemaChangeSchemaVersion、TestMultiSchemaChangeMixedWithUpdate、TestMultiSchemaChangeBlockedByRowLevelChecksum等分别验证回滚语义、并行多变更、schema 版本生成、与 DML 混跑、限制条件等行为tests/realtikvtest/addindextest/multi_schema_change_test.go真实 TiKV 环境下的集成测试覆盖物理执行链路。总结Multi-Schema Change 的设计精髓在于不引入并行的 DDL 执行而是在单一 DDL Job 内部构造子任务 两阶段发布的抽象。数据结构上通过Job.MultiSchemaInfo.SubJobs把多项变更打包持久化执行上通过onMultiSchemaChange与 proxy Job 机制复用既有单 DDL 引擎一致性上通过可回滚状态累积 → 统一推进到不可回滚点 → 串行收尾模拟 2PC 语义。理解了这条主线无论是排查回滚行为、研读在线 DDL 框架还是设计 TiDB 上类似的分层 DDL 任务都能事半功倍。若想继续深入建议按本文引用的源码路径逐步阅读 pkg/ddl/multi_schema_change.go、pkg/meta/model/job.go 与对应的两组测试文件。【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表