1
2
2
2
博客/.../

TiDB 7.5 DM 全量与增量迁移:配置、状态与校验

 拍脑袋小助手  发表于  2026-08-28
原创测试

前言

上一篇文章检查了 TiDB 7.5 与 MySQL 在 SQL、事务、自增主键和排序规则等方面的差异。兼容性确认以后,还要考虑如何把 MySQL 中已有的数据迁入 TiDB,以及迁移期间新增的数据怎样继续同步。

准备 DM 前,先核对 DM 与上游 MySQL 的版本兼容关系。上一篇用的是 MySQL 8.0,但 TiDB 7.5 的 DM 兼容性目录把 MySQL 8.0 标为实验支持,不建议用于生产。这次的测试源库改用 MySQL 5.7.44。从 DM v7.6.0 开始,MySQL 8.0 迁移才转为正式支持。这次的内容只是在测试环境做下全量导入、增量同步、任务状态判断和数据校验,对TiDB的DM做个了解,不是生产系统的切换。

1. 示例环境与验证范围

下面是一套部署在虚拟机中的单源 MySQL 迁移到 TiDB 的示例环境。

项目 地址 版本或说明
MySQL 源库 192.168.56.130:3306 MySQL 5.7.44
DM-master 192.168.56.111:8261 v7.5.7
DM-worker 192.168.56.112:8262 v7.5.7
TiDB 192.168.56.101:4000 v7.5.7
示例库 his_source 模拟业务数据

因为是个人虚拟机环境,所以主要是测试操作流程,任务的执行速度比较一般。

DM 官方把单个 MySQL 实例到 TiDB 的全量加增量迁移列为常见场景。对于小于 1 TiB 的分片数据,官方最佳实践也建议使用 DM 迁移合并。实际选型还要看数据量、日常变更量、迁移窗口和上游负载。

2. 先检查 MySQL binlog

DM 做全量迁移时需要读取源库数据,进入增量阶段后则要依赖 MySQL binlog。

先在源库检查相关参数:

SHOW VARIABLES
WHERE Variable_name IN (
    'server_id',
    'log_bin',
    'binlog_format',
    'binlog_row_image',
    'gtid_mode',
    'enforce_gtid_consistency'
);

示例参数如下:

server_id                130
log_bin                  ON
binlog_format            ROW
binlog_row_image         FULL
gtid_mode                ON
enforce_gtid_consistency ON

增量同步主要看 binlog_format=ROWbinlog_row_image=FULL。DM v7.5 的增量迁移要求 MySQL 开启 binlog,只支持 ROW 格式,同时要求 binlog_row_image=FULL。由于 task-mode: all 同时包含全量和增量,这些项目也会进入前置检查。

GTID 并不是所有单机迁移场景的强制条件,不过官方建议非 Aurora 环境使用 GTID。如果上游还存在主从切换,使用 GTID 管理后续的复制位置会更合适。

binlog_format、GTID 和 binlog 保留策略不要等到迁移窗口临近时才改。参数的修改方式和生效范围需要按 MySQL 版本确认,也要提前核对已有 binlog 是否满足要求。迁移时间较长时,binlog 的保留周期必须覆盖整个迁移过程。DM 尚未读取的日志一旦被清理,增量任务就无法从原位置继续。

3. 为迁移单独创建账号

MySQL 端创建一个 DM 专用账号:

CREATE USER 'dm_sync'@'192.168.56.%'
IDENTIFIED BY 'Test_DM_2026!';

GRANT RELOAD,
      REPLICATION SLAVE,
      REPLICATION CLIENT
ON *.*
TO 'dm_sync'@'192.168.56.%';

GRANT SELECT
ON his_source.*
TO 'dm_sync'@'192.168.56.%';

FLUSH PRIVILEGES;

对于自建 MySQL,DM 官方列出的主要上游权限包括 SELECTRELOADREPLICATION SLAVEREPLICATION CLIENT。如果源端是某些不允许执行 FTWRL 的托管 MySQL,还可能需要 LOCK TABLES 权限。

TiDB 端也使用单独账号:

CREATE USER 'dm_target'@'192.168.56.%'
IDENTIFIED BY 'Test_DM_Target_2026!';

GRANT SELECT,
      INSERT,
      UPDATE,
      DELETE,
      CREATE,
      DROP,
      ALTER,
      INDEX
ON his_source.*
TO 'dm_target'@'192.168.56.%';

上面的密码只是示例。实际环境不要在 DM 配置文件中长期保存明文密码,可以使用下面的命令生成密文:

tiup dmctl encrypt 'password'

生成后,将密文写入 source.yaml 和任务配置。官方示例也采用这种方式。

4. 准备模拟业务表

只用一张表,可以验证最基本的数据搬运流程,但不便于检查表之间的业务关联。这里准备门诊就诊、收费明细和医嘱三类表,通过 visit_no 保留关联关系。

4.1 门诊就诊表

CREATE DATABASE his_source
DEFAULT CHARACTER SET utf8mb4;

USE his_source;

CREATE TABLE outpatient_visit (
    id             BIGINT NOT NULL AUTO_INCREMENT,
    visit_no       VARCHAR(32) NOT NULL,
    patient_no     VARCHAR(32) NOT NULL,
    dept_code      VARCHAR(20) NOT NULL,
    visit_type     VARCHAR(20) NOT NULL,
    visit_time     DATETIME NOT NULL,
    visit_status   VARCHAR(20) NOT NULL,
    update_time    DATETIME NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uk_visit_no (visit_no),
    KEY idx_patient_no (patient_no),
    KEY idx_visit_time (visit_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

写入模拟数据:

INSERT INTO outpatient_visit
    (visit_no, patient_no, dept_code, visit_type,
     visit_time, visit_status, update_time)
VALUES
    ('T202608260001', 'TESTP000001', 'CARD', 'OUTPATIENT',
     '2026-08-26 08:01:15', 'FINISHED', NOW()),

    ('T202608260002', 'TESTP000002', 'ORTH', 'OUTPATIENT',
     '2026-08-26 08:03:26', 'WAITING', NOW()),

    ('T202608260003', 'TESTP000003', 'PED', 'OUTPATIENT',
     '2026-08-26 08:05:41', 'FINISHED', NOW());

校验迁移关系时只需要脱敏后的患者编号,没有必要在示例中加入姓名等身份识别字段。

4.2 收费明细表

CREATE TABLE charge_detail (
    id            BIGINT NOT NULL AUTO_INCREMENT,
    charge_no     VARCHAR(32) NOT NULL,
    visit_no      VARCHAR(32) NOT NULL,
    item_type     VARCHAR(20) NOT NULL,
    item_code     VARCHAR(32) NOT NULL,
    item_name     VARCHAR(100) NOT NULL,
    qty           DECIMAL(10,2) NOT NULL,
    unit_price    DECIMAL(12,2) NOT NULL,
    amount        DECIMAL(12,2) NOT NULL,
    charge_time   DATETIME NOT NULL,
    charge_status VARCHAR(20) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uk_charge_no (charge_no),
    KEY idx_visit_no (visit_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

写入模拟数据:

INSERT INTO charge_detail
    (charge_no, visit_no, item_type, item_code,
     item_name, qty, unit_price, amount,
     charge_time, charge_status)
VALUES
    ('C202608260001', 'T202608260001',
     'REG', 'TEST-REG01', '普通门诊诊查费',
     1, 15.00, 15.00,
     '2026-08-26 08:02:01', 'PAID'),

    ('C202608260002', 'T202608260001',
     'LAB', 'TEST-LAB01', '测试检验项目',
     1, 46.00, 46.00,
     '2026-08-26 08:10:18', 'PAID'),

    ('C202608260003', 'T202608260003',
     'DRUG', 'TEST-DRUG01', '测试药品A',
     2, 18.50, 37.00,
     '2026-08-26 08:13:22', 'PAID');

4.3 医嘱表

CREATE TABLE doctor_order (
    id            BIGINT NOT NULL AUTO_INCREMENT,
    order_no      VARCHAR(32) NOT NULL,
    visit_no      VARCHAR(32) NOT NULL,
    order_type    VARCHAR(20) NOT NULL,
    order_code    VARCHAR(32) NOT NULL,
    order_name    VARCHAR(100) NOT NULL,
    start_time    DATETIME NOT NULL,
    order_status  VARCHAR(20) NOT NULL,
    update_time   DATETIME NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uk_order_no (order_no),
    KEY idx_visit_no (visit_no),
    KEY idx_start_time (start_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

写入两条模拟医嘱:

INSERT INTO doctor_order
    (order_no, visit_no, order_type, order_code,
     order_name, start_time, order_status, update_time)
VALUES
    ('O202608260001', 'T202608260001',
     'LAB', 'TEST-LAB01', '测试检验项目',
     '2026-08-26 08:08:20', 'EXECUTED', NOW()),

    ('O202608260002', 'T202608260003',
     'DRUG', 'TEST-DRUG01', '测试药品A',
     '2026-08-26 08:12:05', 'ACTIVE', NOW());

这三张表用来模拟医院数据库中常见的“就诊与收费”和“就诊与医嘱”两组关联。

5. 无主键表需要单独评估

医院老系统的接口日志表和临时记录表中,经常能看到没有主键和唯一索引的旧表,例如:

CREATE TABLE interface_message_log (
    msg_id          VARCHAR(40) NOT NULL,
    interface_code  VARCHAR(30) NOT NULL,
    send_time       DATETIME NOT NULL,
    message_status  VARCHAR(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这类表在 MySQL 中可以正常使用,但进行 DM 增量同步时会带来风险。前置检查发现上游表没有主键或唯一键时,DM 会给出警告:同一行可能被重复同步,复制性能也可能下降。

这里给确认不存在重复值的模拟表补上主键,不使用 ignore-checking-items 绕过检查:

ALTER TABLE interface_message_log
ADD PRIMARY KEY (msg_id);

生产环境不能直接照抄这条 ALTER TABLE。大表在线增加主键会占用资源,也可能影响业务;历史日志表中还可能已经存在重复值。DBA 需要先检查数据分布、表大小和业务用途,再决定是补主键或唯一索引、改为离线迁移,还是把该表排除在在线同步任务之外。具体怎么处理,要看表的实际情况。

6. 加载 MySQL 数据源

DM 数据源配置文件如下:

source-id: "mysql-his-01"

enable-gtid: true

from:
  host: "192.168.56.130"
  port: 3306
  user: "dm_sync"
  password: "******"

密码字段可以填写下面这条命令生成的密文:

tiup dmctl encrypt 'Test_DM_2026!'

加载数据源:

tiup dmctl \
  --master-addr 192.168.56.111:8261 \
  operate-source create mysql-his-01.yaml

查看数据源状态:

tiup dmctl \
  --master-addr 192.168.56.111:8261 \
  operate-source show

返回结果中要确认 source-id=mysql-his-01,同时检查数据源是否已经由 DM 管理、DM-worker 是否完成绑定。文中没有附实际命令输出,这两项以执行环境中的返回结果为准。

DM 使用 source-id 唯一标识一个 MySQL/MariaDB 实例或复制组,后面的任务配置也通过这个字段引用上游数据源。

7. 使用 all 模式配置任务

这次既要迁移现有数据,也要继续同步迁移期间 MySQL 新产生的数据,任务模式使用:

task-mode: "all"

完整的示例任务配置如下:

name: "his-migrate"
task-mode: "all"

target-database:
  host: "192.168.56.101"
  port: 4000
  user: "dm_target"
  password: "******"

mysql-instances:
  - source-id: "mysql-his-01"
    block-allow-list: "his-rule"

block-allow-list:
  his-rule:
    do-dbs: ["his_source"]

mydumpers:
  global:
    threads: 4
    chunk-filesize: 64

允许列表中只有 his_source,MySQL 实例里的其他数据库不会进入这个任务。

task-mode: all 会同时执行全量和增量迁移。三个模式分别是:

  • full:全量迁移
  • incremental:增量迁移
  • all:全量迁移加增量迁移

示例中的 dump 线程数为 4,不作为推荐值。dump 线程数会影响 DM 到上游 MySQL 的连接数,线程过多会增加源库负载。生产环境要根据压测和监控结果调整,不能只看 DM 节点还有多少空闲 CPU。

8. 启动任务前执行 check-task

任务启动前先执行:

tiup dmctl \
  --master-addr 192.168.56.111:8261 \
  check-task his-migrate.yaml

start-task 会自动进行前置检查,DBA 也可以提前手工执行 check-task,先把问题处理完,再安排迁移窗口。

对于 task-mode: all,DM 会同时检查全量和增量迁移需要的条件,主要包括:

  • 上游数据库版本
  • 表结构兼容性
  • 主键和唯一键
  • dump 权限
  • replication 权限
  • binlog 是否开启
  • binlog_format 是否为 ROW
  • binlog_row_image 是否为 FULL
  • 上游是否存在尚未完成的 Online DDL

检查结果分为警告和必须处理的错误。binlog_formatbinlog_row_image 关系到增量数据是否完整。DM 6.0 及以后的版本不允许通过配置忽略这两项,应直接处理参数或权限问题,不要依赖 ignore-checking-items 强行启动任务。

警告也不能直接略过,要逐项确认影响。没有评估清楚,就不要进入业务切换阶段。

9. 从任务状态判断迁移阶段

前置检查通过后,可以启动任务:

tiup dmctl \
  --master-addr 192.168.56.111:8261 \
  start-task his-migrate.yaml

查询任务状态:

tiup dmctl \
  --master-addr 192.168.56.111:8261 \
  query-status his-migrate

query-status 返回结果里的 unit 表示当前处理单元。all 模式依次经过 DumpLoadSync 三个阶段。

  • Dump:从 MySQL 导出数据
  • Load:把全量数据导入 TiDB
  • Sync:读取并同步 MySQL binlog

当状态进入 unit: Sync 时,任务已经转入 binlog 增量同步阶段。

在 TiDB 中执行 SHOW TABLES;,即使已经看到了业务表,也不能据此判断全量迁移完成,因为导入过程中表结构可能先被创建出来。

某个时点执行 SELECT COUNT(*) 得到相同的行数,只能说明查询时点的数量一致。任务是否进入增量阶段、是否追上上游,还要结合任务状态、binlog 位置和数据校验结果一起判断。

10. 模拟全量迁移后的增量变更

任务进入 Sync 后,MySQL 仍是唯一写入端。此时不要在 TiDB 端写入模拟业务数据,以免两端同时修改同一批数据。下面的变更均在 MySQL 执行。

新增一次就诊:

INSERT INTO his_source.outpatient_visit
    (visit_no, patient_no, dept_code, visit_type,
     visit_time, visit_status, update_time)
VALUES
    ('T202608260004', 'TESTP000004', 'NEURO',
     'OUTPATIENT',
     '2026-08-26 09:15:23',
     'WAITING',
     NOW());

新增一笔收费:

INSERT INTO his_source.charge_detail
    (charge_no, visit_no, item_type, item_code,
     item_name, qty, unit_price, amount,
     charge_time, charge_status)
VALUES
    ('C202608260004',
     'T202608260004',
     'REG',
     'TEST-REG01',
     '普通门诊诊查费',
     1,
     15.00,
     15.00,
     '2026-08-26 09:16:02',
     'PAID');

新增一条医嘱:

INSERT INTO his_source.doctor_order
    (order_no, visit_no, order_type, order_code,
     order_name, start_time, order_status, update_time)
VALUES
    ('O202608260003',
     'T202608260004',
     'LAB',
     'TEST-LAB02',
     '测试检验项目B',
     '2026-08-26 09:20:11',
     'ACTIVE',
     NOW());

再更新一条原本处于候诊状态的记录:

UPDATE his_source.outpatient_visit
SET visit_status = 'FINISHED',
    update_time = NOW()
WHERE visit_no = 'T202608260002';

DM 的 Sync 单元读取 MySQL binlog event,经过任务中的过滤、路由和转换规则处理后写入 TiDB。这些 SQL 只是示例,是否同步成功仍要通过任务状态和下游查询确认。

11. 进入 Sync 后检查复制位置

继续使用下面的命令查看增量同步状态:

tiup dmctl \
  --master-addr 192.168.56.111:8261 \
  query-status his-migrate

进入 Sync 后,不能只看 taskStatus: Runningunit: Sync,还要关注下面这些字段:

  • masterBinlog:上游当前的 binlog 位置
  • syncerBinlog:DM 已经同步到的 binlog 位置
  • masterBinlogGtid:上游当前的 GTID 信息
  • syncerBinlogGtid:DM 已经同步到的 GTID 信息
  • synced:增量复制是否追上上游

synced=false 不一定代表已经出现业务延迟。DM 的后台 checkpoint 并不是实时刷新,判断时还要比较 binlog position、GTID,并结合 DM 监控查看。

正式迁移窗口中,只看到 taskStatus: Running 不能说明已经具备切换条件。

12. 核对本轮增量变更

确认增量同步位置已经追平后,可以在 TiDB 查询刚才发生变化的业务键。

检查新增的就诊记录:

SELECT
    visit_no,
    patient_no,
    dept_code,
    visit_status
FROM his_source.outpatient_visit
WHERE visit_no = 'T202608260004';

检查收费记录:

SELECT
    charge_no,
    visit_no,
    item_type,
    amount,
    charge_status
FROM his_source.charge_detail
WHERE charge_no = 'C202608260004';

检查医嘱记录:

SELECT
    order_no,
    visit_no,
    order_type,
    order_code,
    order_status
FROM his_source.doctor_order
WHERE order_no = 'O202608260003';

检查那条被更新的候诊记录:

SELECT
    visit_no,
    visit_status
FROM his_source.outpatient_visit
WHERE visit_no = 'T202608260002';

单独比较 SELECT COUNT(*),发现不了字段值不一致,也不能证明跨表关系正确。

13. 按业务口径补充校验

行数只能作为基础检查。对医院业务数据,还要核对收费金额、状态分布和跨表关系。

收费数据可以按日期汇总:

SELECT
    DATE(charge_time) AS charge_date,
    COUNT(*) AS detail_count,
    SUM(amount) AS total_amount
FROM his_source.charge_detail
GROUP BY DATE(charge_time)
ORDER BY charge_date;

门诊记录按状态统计:

SELECT
    visit_status,
    COUNT(*) AS cnt
FROM his_source.outpatient_visit
GROUP BY visit_status
ORDER BY visit_status;

医嘱也按状态统计:

SELECT
    order_status,
    COUNT(*) AS cnt
FROM his_source.doctor_order
GROUP BY order_status
ORDER BY order_status;

还可以检查跨表关联。下面这条 SQL 用于查询没有对应就诊记录的收费明细:

SELECT COUNT(*) AS orphan_charge_count
FROM his_source.charge_detail c
LEFT JOIN his_source.outpatient_visit v
       ON c.visit_no = v.visit_no
WHERE v.visit_no IS NULL;

医嘱表采用同样的检查方法:

SELECT COUNT(*) AS orphan_order_count
FROM his_source.doctor_order o
LEFT JOIN his_source.outpatient_visit v
       ON o.visit_no = v.visit_no
WHERE v.visit_no IS NULL;

这些 SQL 是业务层面的辅助检查,不能代替专业的数据一致性工具。如果发现收费金额不一致、医嘱状态数量异常或存在孤立记录,即使 DM 没有报错,也要暂停后续步骤并查明原因。生产环境要按 HIS、EMR、LIS、PACS、医保和财务等系统的实际对账规则设计核对项。

14. 使用 sync-diff-inspector 校验数据

DM 最佳实践建议在迁移后使用 sync-diff-inspector 比较上下游数据。该工具对 MySQL 和 TiDB 做全量校验时,不支持校验范围持续发生变化。可以停止相关表在上游的写入,也可以通过 range 只比较不会再变化的数据范围。

执行全表校验前,确认 DM 已经追上上游。配置文件可以直接引用 DM 任务:

check-thread-count = 4
export-fix-sql = true
check-struct-only = false

dm-addr = "http://192.168.56.111:8261"
dm-task = "his-migrate"

[task]
output-dir = "./his-diff-output"
target-check-tables = [
    "his_source.outpatient_visit",
    "his_source.charge_detail",
    "his_source.doctor_order",
    "his_source.interface_message_log"
]

执行命令:

./sync_diff_inspector \
  --config=./his-diff.toml

TiDB v7.5 支持 sync-diff-inspector 从 DM-master 读取任务配置,这样不需要重复维护上下游映射。工具可以比较表结构和表数据,也能为少量差异生成修复 SQL。

修复 SQL 只能作为排查材料。涉及收费、医嘱和患者状态的差异时,必须先查明原因并完成业务审核,不能直接执行。

如果源库必须持续写入,可以评估 DM v7.5 提供的增量数据校验功能。它用于检查增量同步中的变化行,但要求被校验的表具备主键或非空唯一键,对部分数据类型和 DDL 也有限制。全量校验用于静态数据范围;数据仍在变化时,再考虑增量校验。

15. 应用切换需要单独设计

这里不直接把应用连接从 192.168.56.130:3306 改到 192.168.56.101:4000

数据同步完成,并不代表应用已经具备切换条件。医院业务切库要先确定停写窗口。停写以后,再确认 DM 已经追平,并完成最终数据核对。数据库连接、连接池、账号权限、字符集和事务参数也要按新环境重新检查。

上一篇提到的自增 ID 问题,在 DM 切换场景中仍然存在。增量同步期间,上游 MySQL 生成的自增值会作为显式值写入 TiDB。应用切到 TiDB 后,如果改由 TiDB 分配 ID,应按照官方 AUTO_INCREMENT 文档检查并清理相关 TiDB Server 的自增 ID 缓存,避免与已经同步的数据冲突。具体处理命令要以目标集群版本和表定义为准。

回退路径也要在切换前定下来。应用开始写入 TiDB 后,如果出现关键业务异常,是直接切回原 MySQL,还是先处理 TiDB 中新产生的数据再回切,会影响整套切换方案。这个问题需要单独演练,不能等故障发生后再临时决定。

16. 迁移过程中需要保留的检查记录

DM 常用命令:

  • operate-source create:加载数据源
  • check-task:检查任务
  • start-task:启动任务
  • query-status:查询状态

任务启动前,先记录 DM 与上游 MySQL 的版本支持关系、binlog 参数、账号权限和无主键表处理结果。任务运行以后,保存 query-status、binlog position、GTID、告警和最终校验结果。门诊数量、收费金额、医嘱状态和跨表关系等业务核对记录也要一并归档。

1
2
2
2

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论