---
title: 'DM 分库分表 DDL “乐观协调”模式介绍丨 TiDB 工具'
date: '2021-12-22T00:00:00+08:00'
status: publish
permalink: /blog/introduction-to-the-dm-ddl-optimistic-coordination-model
author:
    - 郭铭浩
    - 郭铭浩
    - 郭铭浩
excerpt: '本文主要介绍 TiDB DM 分库分表 DDL 协调的“乐观协调”模式。乐观协调模式适用于上游灰度更新、发布的场景，或者是对上游数据库表结构变更过程中同步延迟比较敏感的场景。'
type: post
id: 3081
category:
    - 产品技术解读
tag:
    - DM
    - 数据迁移
post_format: []
profile:
    - 郭铭浩
algolia_posts_post_records_count:
    - '4'
---
前言
--

DM 支持在线执行分库分表的 DDL 语句（通称 Sharding DDL），[先前的文章](https://cn.pingcap.com/blog/introduction-to-the-dm-ddl-pessimistic-coordination-model)中，我们介绍了悲观模式，即当上游一个分表执行某一 DDL 后，这个分表的迁移会暂停，等待其他所有分表都执行了同样的 DDL 才在下游执行该 DDL 并继续数据迁移。

悲观协调模式的优点是可以保证迁移到下游的数据不会出错，并且能兼容大部分的 DDL 语句，缺点是会暂停数据迁移而不利于对上游进行灰度变更、并显著地增加增量数据复制的延迟。有些客户可能会花数个月在单一分片执行 DDL，满意后才会更改其他分片的结构。在悲观同步的设定下，用来测试的分片的 DML 事件会大量积压，在恢复同步后无法正常运作。与此同时，悲观模式还要求所有分片必须以相同的顺序执行 DDL，否则会导致任务报错暂停。

为此，DM 提供新的乐观协调模式，在一个分表上执行的 DDL，自动修改成兼容其他分表的 DDL 语句后立即应用到下游，不会阻挡任何分表执行的 DML 的迁移。**乐观协调模式适用于上游灰度更新、发布的场景，或者是对上游数据库表结构变更过程中同步延迟比较敏感的场景。**

![1.png](https://www-website-strapi.oss-cn-shanghai.aliyuncs.com/prod/1_8e4e429342.png)<center>悲观协调和乐观协调的对比</center>原理
--

DM worker 的所有 DML 会直接同步到下游（出错时例外）。

DM worker 内嵌了一个小型 TiDB（通称 schema tracker），用来记录各个上游分表的表结构，当接收到来自上游的 DDL 后，会根据 schema tracker 里 DDL 的执行结果，把更新后的表结构转送给 DM master。DM master 将收到的不同分片的表结构合并成可兼容所有分片的 DML 的合成结构，即不同分片表结构的并集（此过程类似于 SQL 语句中的 JOIN 语句），然后根据合成的表结构和 DM worker 发来的表结构的不同处得到对应的 DDL 语句（即合成的表结构与原表结构的差集），同步到下游。

![2.png](https://www-website-strapi.oss-cn-shanghai.aliyuncs.com/prod/2_fba427a4e8.png)具体的设计可以参考 [DM: Manage DDLs on Sharded Tables by Maximizing Schema Compatibility](https://github.com/pingcap/tiflow/blob/master/dm/docs/RFCS/20191209_optimistic_ddl.md).

规则
--

乐观 DDL 表结构合并的规则简单来说就是对列属性定义了一个**偏序关系**，对不同表的同一列进行排序，选择该偏序关系中的极大元。对于不可比较的列，则返回错误

- null < not null
- no default < default(x)
- varchar(x) < varchar(y), where x< y
- utf8 < utf8mb4
- char < varchar
- tinyint < smallint < mediumint < bigint
- …

对于被不存在或者被删除的列，我们把它定为最小的列

如初始时表结构是相同的。

![3.png](https://www-website-strapi.oss-cn-shanghai.aliyuncs.com/prod/3_625ecc7432.png)tbl2 添加第三列。前两列相同；tbl1 的第三列为空，所以保留 tbl2 的第三列。

![4.png](https://www-website-strapi.oss-cn-shanghai.aliyuncs.com/prod/4_0aceaf7579.png)tbl2 删除第一列。第二列相同；tbl2 的第一列为空，所以保留 tbl1 的第一列。tbl1 的第三列为空，所以保留 tbl2 的第三列

![5.png](https://www-website-strapi.oss-cn-shanghai.aliyuncs.com/prod/5_c9c6e80b29.png)tbl1 将第二列改为 varchar(10)，由于 varchar(5) < varchar(10)，所以保留 tbl1 的第二列

![6.png](https://www-website-strapi.oss-cn-shanghai.aliyuncs.com/prod/6_3dfca1c2a0.png)tbl1 重命名第二列。现在 tbl1 和 tbl2 的第二列名字不一样，无法比较，DM 无法确定最终的表结构，所以任务会报错

![7.png](https://www-website-strapi.oss-cn-shanghai.aliyuncs.com/prod/7_2fc62ed4e4.png)例子
--

三个分片合并同步到 TiDB

![8.png](https://www-website-strapi.oss-cn-shanghai.aliyuncs.com/prod/8_9fe638fc8a.png)① 在上游增加一列 Level。  
alter table `tbl00` add column `Level` int unsigned not null;

![9.png](https://www-website-strapi.oss-cn-shanghai.aliyuncs.com/prod/9_27dc102f42.png)tbl00, tbl01, tbl02 的并集 tblMerge 是 {ID,NAME,Level}  
tblMerge 和 tbl 的差集是 {Level}，所以 DDL 是 add column Level

此时下游 TiDB 要准备接受来自 tbl00 有 Level 的 DML、以及来自 tbl01 和 tbl02 没有 Level 的 DML，所以同步到下游时，自动改写成指定默认值的形式。  
alter table `tbl` add column `Level` int unsigned not null default 0;

![10.png](https://www-website-strapi.oss-cn-shanghai.aliyuncs.com/prod/10_50031038ff.png)这时候各种 DML 毋需修改都可以同步到下游。  
update `tbl00` set `Level` = 9 where `ID` = 1;  
insert into `tbl02` (`ID`, `Name`) values (27, 'Tony');

![11.png](https://www-website-strapi.oss-cn-shanghai.aliyuncs.com/prod/11_94b6616a1c.png)② 在 tbl01 同样增加一列 Level。  
alter table `tbl01` add column `Level` int unsigned not null;

![12.png](https://www-website-strapi.oss-cn-shanghai.aliyuncs.com/prod/12_9227f39155.png)tbl00, tbl01, tbl02 的并集 tblMerge 是 {ID,NAME,Level}  
tblMerge 和 tbl 的差集是 {}，所以 DDL 为空

此时下游已经有相同的 Level 列了，所以 DM master 比较之后不做任何动作。

③ 在 tbl01 刪除一列 Name。  
alter table `tbl01` drop column `Name`;

![13.png](https://www-website-strapi.oss-cn-shanghai.aliyuncs.com/prod/13_3dbf7c870f.png)tbl00, tbl01, tbl02 的并集 tblMerge 是 {ID,NAME,Level}  
tblMerge 和 tbl 的差集是 {Level}，所以 DDL 为空

此时下游仍需要接收来自 tbl00 和 tbl02 含 Name 的 DMLs，故不立删之，而是为这列也补上一个默认值。  
alter table `tbl` alter column `Name` set default “”;

同样，各种 DML 仍可直接同步到下游。  
insert into `tbl01` (`ID`, `Level`) values (15, 7);  
update `tbl00` set `Level` = 5 where `ID` = 5;

![14.png](https://www-website-strapi.oss-cn-shanghai.aliyuncs.com/prod/14_fde7e36490.png)④ 在 tbl02 增加一列 Level。  
tbl00, tbl01, tbl02 的并集 tblMerge 是 {ID,NAME,Level}  
tblMerge 和 tbl 的差集是 {Level}，所以 DDL 为空  
alter table `tbl02` add column `Level` int unsigned not null;

![15.png](https://www-website-strapi.oss-cn-shanghai.aliyuncs.com/prod/15_2dc2edfb97.png)此时所有分片都已有 Level 列，所以可以把作为兼容的默认值去掉。  
alter table `tbl` alter column `Level` drop default;

⑤⑥ 在 tbl00 和 tbl02 各刪除一列 Name。  
alter table `tbl00` drop column `Name`;  
alter table `tbl02` drop column `Name`;

![16.png](https://www-website-strapi.oss-cn-shanghai.aliyuncs.com/prod/16_fb6df0df51.png)tbl00, tbl01, tbl02 的并集 tblMerge 是 {ID,Level}  
tblMerge 和 tbl 的差集是 -{Name}，此差集是有符号的，所以 DDL 是 drop column Name

到此步 Name 列也从所有分片消失了，所以可以安全从下游移除。  
alter table `tbl` drop column `Name`;

![17.png](https://www-website-strapi.oss-cn-shanghai.aliyuncs.com/prod/17_fe14c8d864.png)限制
--

**使用“乐观协调”模式有一定的风险，需要严格遵照以下方针：**

- 执行每个批次的 DDL 前和后，要确保每个分表的结构达成一致。
- 进行灰度 DDL 时，最好只集中在一个分表上测试。
- 灰度完成后，在其他分表上尽量以最简单直接的 DDL 迁移到最终的 schema，而不要重新执行灰度测试中对或错的每一步。 
  - 例如：在分表执行过 ADD COLUMN A INT; DROP COLUMN A; ADD COLUMN A FLOAT;，在其他分表直接执行 ADD COLUMN A FLOAT 即可，不需要三条 DDL 都执行一遍。
- 执行 DDL 时要注意观察 DM 迁移状态。当迁移报错时，需要判断这个批次的 DDL 是否会造成数据不一致。

更详细的介绍可参考[官网文档](https://docs.pingcap.com/zh/tidb-data-migration/stable/feature-shard-merge-optimistic)