前言
上一篇文章检查了 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=ROW 和 binlog_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 官方列出的主要上游权限包括 SELECT、RELOAD、REPLICATION SLAVE 和 REPLICATION 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是否为ROWbinlog_row_image是否为FULL- 上游是否存在尚未完成的 Online DDL
检查结果分为警告和必须处理的错误。binlog_format 和 binlog_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 模式依次经过 Dump、Load 和 Sync 三个阶段。
Dump:从 MySQL 导出数据Load:把全量数据导入 TiDBSync:读取并同步 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: Running 和 unit: 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、告警和最终校验结果。门诊数量、收费金额、医嘱状态和跨表关系等业务核对记录也要一并归档。