☰
dbswitch实战:MySQL到PostgreSQL全量迁移与增量同步
2026/9/26 18:20:36 网站建设 项目流程

简介:dbswitch工具是一个面向数据库开发与运维人员的开源迁移同步组件,解决源端到目的端数据库的批量结构迁移与数据同步问题,支持全量同步及基于主键表的增量变更同步。资源包含完整项目工程,共506个文件,以306个Java源码为主,辅以36个JavaScript、25个Vue前端页面、20个JAR依赖包、15个XML配置、13个SQL脚本及若干Shell/YAML启动与部署文件,压缩包约99.09MB,目录结构清晰,便于直接阅读改造。核心能力涵盖字段类型转换、主键与建表语句生成、基于正则的表名字段名映射,以及JDBC分批读取与insert/copy方式写入,适合希望掌握数据迁移工具设计思路或二次开发的中高级开发者。目前已有790人浏览学习,资源可用于快速搭建数据库迁移测试环境并验证增量同步效果。

1. 做数据库迁移的人,最后都在跟增量同步较劲

做数据库迁移的人应该都遇到过这个场景:业务方丢过来一句话,说把某套系统从 MySQL 迁到 PostgreSQL,数据要全量过来,完了还要持续同步增量。这句话听着简单,实际是三类活叠在一起:结构转换、全量搬迁、增量追平。dbswitch 就是专门做源端数据库向目的端数据库批量迁移同步的开源工具,核心能力就是全量和增量两条链路全覆盖。它能解决的典型问题包括:异构数据库首次搬迁、多套库之间例行同步、切换窗口内追赶源端写入。这篇文章写给准备用 dbswitch 做迁移、或者已经在迁移半路上的人,我会从全量迁移的结构转换机制讲起,再拆增量同步的原理和配置,最后给出真实环境里的参数和踩坑记录。

2. 全量迁移为什么难在“搬过去就能用”:结构转换和批量搬运的底层逻辑

2.1 结构转换往往比数据搬运先翻车

全量迁移如果只把数据用 JDBC 读出来、插进去,十有八九会死在主键冲突或字段超长上。原因是源端和目标端的表结构大概率不是一回事:MySQL 的datetime到 PostgreSQL 要映射成timestamp,Oracle 的number(10, 2)到国产库要确认精度保留,tinyint(1)在 MySQL 里是布尔语义,到了 PostgreSQL 却变成int2。dbswitch 把结构同步放在数据同步之前,先采集源端元数据,生成统一内部模型,再按目标库方言生成 DDL。

我实际用 dbswitch 跑迁移时,第一遍结构同步很少能一次通过,大多卡在三类问题上:

  • 源库字段是 MySQL 的unsigned int,目标库没有 unsigned 概念,需要降级成bigint,否则迁移完成后上限不够用;
  • 源库大量varchar(n)实际存的是 JSON,目标库如果是 PostgreSQL,建表直接建成jsonb更合理,但工具默认按varchar处理,后续应用改造成本高;
  • 源库enum类型在目标库没有对应,常见做法是展开成varchar(n),n 要取枚举最长值。

所以每次做大规模迁移,我会先让 dbswitch 生成一份结构差异报告,人工过一遍有风险的映射,再进入数据同步。跳过这个步骤直接全库搬迁,看起来省事,实际是把问题推迟到写入阶段集中爆发。

2.2 全量迁移的执行流程与关键配置参数

dbswitch 一次全量迁移,常见做法是走三链路。第一步元数据采集,读源端information_schema或系统目录,拿到表清单、字段、主键、索引、默认值。第二步结构转换,把采集结果映射成目标 DDL 并在目标库执行。第三步数据搬运,按表拆分,通过 JDBC 分批select和批量insert完成任务。

三个环节里数据搬运的问题最好发现,结构转换的问题会藏到写入阶段才爆。所以我会先把include-tables缩到一两张表跑通,验证 DDL 生成符合预期,再放开全库。下面这份 YAML 是 dbswitch 全量迁移的常用配置样例,不同版本字段名略有出入,但配置思想一致。

# dbswitch 全量迁移最小配置示例 source: type: mysql host: 192.168.10.11 port: 3306 username: migrator password: "your_password" database: business_db jdbc-url: jdbc:mysql://192.168.10.11:3306/business_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&rewriteBatchedStatements=true target: type: postgresql host: 192.168.10.21 port: 5432 username: postgres password: "your_password" database: business_db jdbc-url: jdbc:postgresql://192.168.10.21:5432/business_db?stringtype=unspecified table-mapping: source-schema: business_db target-schema: public table-prefix: t_ table-suffix: "" field-prefix: "" field-suffix: "" field-name-case: lower include-tables: - "user_*" - "order*" exclude-tables: - "*_tmp" - "*_2024" batch: batch-size: 1000 read-size: 500 threads: 4

几个最重要参数的使用心得:include-tables和exclude-tables支持通配符,迁移范围靠这两个控制,先跑白名单再逐步放开,比一次全库稳得多。batch-size是每次插入的行数,MySQL 到 PostgreSQL 场景 1000 行一批通常性价比最好,太小浪费往返,太大容易触发目标端锁竞争。threads控制并发表数,不是单表内部并发,单表极大时调线程数没用,得靠主键范围拆分。

rewriteBatchedStatements=true必须显式写在 MySQL 连接串里,mysql 驱动默认批处理是一行一行发 SQL,不打开的话批量插入性能会断崖式下跌。写完后启动命令一般是:

# 进入 dbswitch 安装目录,指定配置文件启动 java -jar dbswitch-cli.jar -c conf/mysql2postgres.yaml # 只生成 DDL 不同步数据,用于检查结构是否符合预期 java -jar dbswitch-cli.jar -c conf/mysql2postgres.yaml --export-ddl /tmp/ddl.sql # 只同步数据,不复用结构阶段,适合目标表已经手工建好的场景 java -jar dbswitch-cli.jar -c conf/mysql2postgres.yaml --data-only

--export-ddl是我非常依赖的能力:先只导出 DDL,人工核对一遍再放数据。它能避免“迁移到一半发现字段类型不够用”的返工。--data-only在目标表已经存在、只想灌数据时很有用。

2.3 类型映射对照表与 DDL 审查习惯

类型映射是结构转换的核心,dbswitch 内置了一套映射规则,常见方向如下表:

源端 MySQL目标端 PostgreSQL说明
TINYINT(1)BOOLEAN按长度 1 自动识别
SMALLINTSMALLINT长度不丢失
MEDIUMINTINTEGERMySQL 独有类型展开
INTINTEGER常规映射
BIGINTBIGINT常规映射
DECIMAL(p, s)NUMERIC(p, s)精度必须保留
DATETIMETIMESTAMP时区需要先统一
TIMESTAMPTIMESTAMPTZ默认带时区
CHAR(n)CHAR(n)长度传递
VARCHAR(n)VARCHAR(n)长度传递,utf8mb4 注意扩列
TEXTTEXT大字段
BLOBBYTEA二进制
ENUMVARCHAR展开成字符串
JSONJSONB可选映射,默认可能为 TEXT

这张表看着简单,异构迁移里真正的坑是“同类型不同语义”。varchar(255)在 MySQL 的 utf8mb4 下最多占 1020 字节,在 PostgreSQL 按字符数存储通常不用加长;但反过来 Oracle 的varchar2(300 char)迁到 MySQL 会因为字节上限触发行溢出,需要拆列。datetime和timestamp的时区处理,迁移前把源端连接串serverTimezone和目标端时区统一,否则增量阶段更容易出现偏差。decimal精度比例不一致时,数据能搬过去,下游报表汇总口径就变了。

这些规则不要求人肉背。dbswitch 结构比较输出会告诉你每个字段映射成什么,关键在于拿到报告后能识别哪些映射是危险的。我的习惯是:看 DDL 时重点扫三处,主键是否保留、decimal 精度是否丢失、大字段是否被截断。这三处过关,全量迁移大概率稳。

3. 增量同步的原理与实操:从日志捕获到目标库回放

3.1 增量方案选型:定时比对为什么不是好选择

全量迁移做完只是第一个里程碑。业务系统还在持续写入,目标库要长时间保持和源端一致,就必须有增量同步机制。市面上常见的增量做法有两种:一种是用定时任务做行级对比,把差异数据补齐;另一种是基于数据库日志的变更捕获,也就是常说的 CDC。很多数据库同步工具都支持 CDC 模式,dbswitch 的增量链路口径也是走日志捕获,而不是轮询比对。

定时比对在数据量小、表结构简单的场景能凑合用,但有一个硬伤:它只能“发现差异”,无法“记录变更”。如果一张表在两次同步间隔内发生了多次更新,最后一次的值覆盖了中间状态,对比任务根本不知道发生了多少次变更。更重要的是,定时对比依赖主键或唯一键,没有主键的表只能全表扫描,性能和时间成本都会失控。所以数据库同步软件真正可用的增量方案,普遍是读取 binlog、redo log 这类日志,把每一次 insert、update、delete 解析成结构化变更事件,再回放到目标端。

3.2 基于日志的增量捕获与断点续传机制

MySQL 的增量捕获通常依赖 binlog,源库需要开启log_bin并设置binlog_format=ROW。ROW 格式下 binlog 记录的是每行变更前后的完整镜像,解析出来的就是“哪张表哪一行被改成了什么”,这正是增量同步需要的粒度。STATEMENT 格式记录的是 SQL 语句本身,解析难度大而且容易产生语义偏差,所以做增量同步之前必须先确认源库 binlog 配置。

日志捕获链路里最关键的是 offset 管理。同步任务启动时记录当前 binlog 文件和位置,消费完一批事件后把 offset 持久化下来,任务重启后从上次记录的位置继续,不重不漏。dbswitch 的增量同步通常把 offset 保存在检查点文件或目标库的专用表里,配置里指定 checkpoint 存储位置即可。如果 checkpoint 和同步数据写入不是同一事务,极端情况下会重复消费,所以下游回放要保证幂等:插入用主键冲突则更新,更新依赖版本号或旧值条件,删除按主键定位。

增量同步还有个全量和增量的衔接问题。常见策略是“先全量、后增量”:全量迁移启动时记录一个 binlog 位点,全量跑完后从该位点开始回放增量。这样全量期间源端的新写入不会丢。dbswitch 的增量模块一般支持这种 full-increment 模式,配置好之后它会自动处理位点衔接。

3.3 增量同步配置样例与关键参数

以下是我用过的增量同步配置写法,核心是通过指定源端 binlog 位点来启动任务:

# dbswitch 增量同步配置示例 cdc: source: type: mysql host: 192.168.10.11 port: 3306 username: cdc_user password: "your_password" database: business_db # 增量任务需要读 binlog,账号必须有 REPLICATION SLAVE 权限 binlog: server-id: 61233 # 全量迁移完成时记录下来的位点 offset: filename: mysql-bin.000128 position: 1563870 target: type: postgresql host: 192.168.10.21 port: 5432 username: postgres password: "your_password" database: business_db schema: public filter: include-tables: - "user_*" exclude-tables: - "*.log_*" checkpoint: type: file path: /data/dbswitch/checkpoint

参数说明:server-id必须设置为一个不和源库其他从库冲突的整数,MySQL 集群里每个 binlog 消费者都有独立 server-id,重复会导致连接被踢。offset.filename和offset.position是启动位点,全量迁移开始时就要记好这两个值,否则全量期间产生的增量数据没有起点。filter里的*.log_*表示所有 schema 下以log_开头的表都过滤掉,日志表、临时表一般不需要同步。checkpoint建议放在本地磁盘或独立表,不要放在源库,否则源库变更会影响同步进度记录。

增量任务启动后会一直常驻,确认它跑起来的标准是看两处:一是 checkpoint 文件的 offset 是否在持续前进,二是源库 binlog 的消费位点没有落后太多。如果 checkpoint 长时间不动,说明解析或回放卡住了,优先查目标端连接和主键冲突。

# 启动增量同步任务,一般会用 nohup 放到后台 nohup java -jar dbswitch-cdc.jar -c conf/cdc_mysql2pg.yaml > logs/cdc.log 2>&1 & # 查看当前消费位点是否在推进 tail -f logs/cdc.log | grep "checkpoint"

增量同步在大部分场景下要比全量更容易出问题,因为它依赖源库日志、目标端事务和外部位点三个环节同时正常。任何一环抖动,都可能导致丢数据或重复数据。所以增量任务必须有监控,不能跑起来就不管。

4. 完整实操:用 dbswitch 把核心订单表从 MySQL 迁到 PostgreSQL

4.1 动手前先做表结构体检

以一套真实的订单系统为例:源端是 MySQL 8.0,目标端是 PostgreSQL 14,业务要求把user_orders和关联的order_items两张表迁过去,并且持续增量同步。迁移前我先做结构体检,目的是发现哪些表能让 dbswitch 自动映射,哪些表需要手工干预。

体检分三步。第一步用 dbswitch 导出源端 DDL,对照目标库手工检查。第二步查大字段和特殊类型,比如user_orders里有一个remark TEXT,还有一个order_status ENUM('pending','paid','shipped','cancelled'),这两处在 PostgreSQL 里分别对应TEXT和VARCHAR。第三步确认主键和唯一索引,增量同步必须有明确的键来保证幂等,没有主键的表要提前补主键或唯一约束。

做完体检发现order_items的price DECIMAL(10,2)映射到 NUMERIC(10,2) 没问题,但user_orders.create_time是DATETIME并且业务代码大量依赖这个字段做分页排序。目标端建表时我决定手工改成TIMESTAMP WITH TIME ZONE,并预先告诉业务方时间语义的变化。这种调整必须在全量迁移前完成,迁移后再改字段类型,代价是重建整张表。

4.2 全量迁移执行与行数核对

结构确认无误后,写全量迁移配置。两张表不需要全库迁移,所以include-tables只保留这两张,table-prefix设成t_,这样目标端表名分别是t_user_orders和t_order_items。

# 订单表全量迁移配置 table-mapping: source-schema: business_db target-schema: public table-prefix: t_ field-name-case: lower include-tables: - "user_orders" - "order_items" batch: batch-size: 1000 threads: 2

启动之前先做两件事:记录源库 binlog 位点,以及记录两张表的源端行数。位点是增量同步的起点,行数是全量迁移后的核对基准。

# 记录源端 binlog 位点 mysql -h 192.168.10.11 -u migrator -p -e "SHOW MASTER STATUS;" # 源端行数与目标端行数核对 mysql -h 192.168.10.11 -u migrator -p -e "SELECT COUNT(*) FROM business_db.user_orders;" psql -h 192.168.10.21 -U postgres -d business_db -c "SELECT COUNT(*) FROM public.t_user_orders;"

执行全量迁移:

java -jar dbswitch-cli.jar -c conf/order_migration.yaml

执行完看日志里每张表的迁移行数,然后对比刚才记录的行数。行数一致不代表数据完全一致,还要抽查几条关键记录的字段值。我的习惯是取源端最大时间那几条,比对目标端是否存在,同时取一个中间随机 id 做全字段比对。这一步过了,全量迁移才算完成。

4.3 接入增量同步并验证追上源端

全量迁移完成后,立刻启动增量同步任务,起始位点就是全量开始前记录的那个 binlog 位置。

# 增量配置复用前面示例,只改 offset 为刚才记录的位点 cdc: source: username: cdc_user binlog: server-id: 61234 offset: filename: mysql-bin.000128 position: 1563870 target: type: postgresql host: 192.168.10.21

启动后验证增量链路是否真正工作,最常见的方式是在源端造一条测试数据:

-- 源端插入一条测试订单 INSERT INTO user_orders (id, user_id, amount, status, remark) VALUES (999001, 88, 29.90, 'paid', 'cdc_verify'); -- 等 3~5 秒后在目标端查询 SELECT * FROM t_user_orders WHERE id = 999001;

这条测试数据在全量迁移之后插入,如果增量同步正常,几秒内就能在目标端查到。查不到就说明增量链路没生效,需要看源库 binlog 是否开启、账号权限是否足够、目标端是否有主键冲突。验证通过后删掉测试数据,再观察增量任务日志里是否有持续的变更事件输出,同时确认 checkpoint 在推进。

增量同步稳定运行两周后,我会做一次回放验证:把源端一周的变更量统计出来,和目标端相应表的更新量对比,差额在可接受范围内才算真正验收。这个验证不追求绝对值完全一致,因为业务读取和写入时间窗口有偏差,但数量级不能差太多。

5. 真实环境避坑:迁移同步的五个高频翻车现场

5.1 大表迁移中途 OOM,任务直接中断

现象:迁移一张 2 亿行的大表时,JVM 频繁 Full GC,最后直接OutOfMemoryError,任务中断。

原因:batch-size设置过大,同时read-size没有限制,JDBC 一次select把所有数据拉进客户端内存,大字段把堆撑爆。另一个隐藏原因是目标端的批量插入在事务未提交时堆积了大量 undo 数据。

解决:把batch-size降到 500~1000,read-size显式设置成 2000 行以内,确保单次读入内存的数据量可控。同时给 JVM 堆一个合理上限,启动命令加上-Xms2g -Xmx4g。最重要的是大表不要单线程跑,按主键范围拆成多个任务并发,每个任务只处理一段数据。

5.2 目标端表已存在导致字段错位

现象:迁移前目标库已经有同名表,但字段顺序和源端不一样。dbswitch 默认按字段名匹配,目标表多了一个源端没有的字段,插入时该字段全是默认值,源端数据反而没能正确写入。

原因:结构同步阶段检测到目标表存在,跳过 DDL 执行,但数据写入时生成的 INSERT 语句没有显式列出字段名,按位置插入导致错位。

解决:迁移前先确认目标表是否已存在,如果存在,要么删掉让 dbswitch 重建,要么在配置里显式指定字段映射关系。我的习惯是目标表一律让工具重建,业务要保留的表单独处理。如果不想删表,就用 SQL 先手工比对字段类型和顺序,确认完全一致再跑数据同步。

5.3 无主键表在增量阶段被跳过或重复

现象:有些业务表没有主键,全量迁移正常,增量同步开始后这些表完全同步不过来,或者出现重复数据。

原因:日志捕获出来的变更事件没法定位具体行,目标端回放时无法判断是新增还是更新。MySQL 的 binlog 在无主键表上只记录完整行镜像,更新操作可能产生多行匹配,直接回放就会把不该改的行改掉。

解决:增量同步涉及的每张表必须有主键或唯一索引。源端没有主键的表,先和业务确认能否补一个自增主键,或者用多个字段组合成唯一键。如果是历史遗留表确实补不了,只能在增量过滤规则里排除,改为定时全量重刷这种低频同步。

5.4 源端 binlog 格式不是 ROW,增量任务只同步了部分数据

现象:增量任务启动后日志没有报错,但目标端数据对不上,部分 update 操作没有生效。

原因:源库binlog_format是STATEMENT或MIXED,日志捕获拿不到完整的行级变更,某些依赖函数和表达式的 SQL 无法正确回放。

解决:修改源库binlog_format=ROW,这一步需要重启 MySQL 实例,要在维护窗口做。修改后确认SHOW VARIABLES LIKE 'binlog_format'返回 ROW,再重新启动增量任务。如果源库是阿里云 RDS 这类托管实例,控制台一般有参数组可以直接改,同样需要重启实例,注意提前评估对业务的影响。

5.5 迁移完成后才发现字符串乱码和字符集不一致

现象:全量迁移结束后,目标端部分中文字符显示为????或乱码,增量阶段新写入的数据正常,旧数据全部异常。

原因:源端连接串没有指定characterEncoding,JDBC 默认按系统字符集读取,写入 PostgreSQL 时又按库默认编码转换,两边没对齐就产生了乱码。

解决:源端连接串显式加characterEncoding=utf8,目标端连接串确认client_encoding=UTF8。迁移前先用一条含中文的测试数据验证编码链路,不要等迁完再检查。如果已经迁移完才发现,只能删除目标端数据,修正连接串后重新迁移全量,增量任务不受影响,但全量必须重跑一遍。

6. 进阶手法:一致性校验与同步性能调优

6.1 三段式一致性校验

迁移完成后的验收,我的方法是三段式校验。第一段是行数对比,两张表分别COUNT(*),数量不一致直接定位漏数据。第二段是校验和对比,对每张表按主键排序后拼接关键字段,算一个哈希值。MySQL 里可以用CRC32,PostgreSQL 里用MD5,两边分别算出聚合值再比对:

-- 源端 MySQL SELECT COUNT(*) AS cnt, SUM(CRC32(CONCAT_WS('|', id, user_id, amount, status))) AS chk FROM user_orders; -- 目标端 PostgreSQL SELECT COUNT(*) AS cnt, SUM(('x' || SUBSTR(MD5(CONCAT_WS('|', id, user_id, amount, status)), 1, 8))::BIT(32)::INT) AS chk FROM t_user_orders;

这里有个细节:MySQL 的CRC32和 PostgreSQL 的 MD5 不能直接等价对账,两边算法不同。我通常的做法是统一在目标端把全表导出后用同一套脚本算哈希,或者只在两边都支持的维度上对比加总和。第三段是抽样明细比对,随机取 100 条主键,逐字段比对源端和目标端的值。三段都通过,迁移才算验收完成。

6.2 同步性能调优的优先级

性能调优我遵循固定的优先级顺序,不会一上来就动并发线程。

第一步调batch-size。MySQL 到 PostgreSQL 的场景,从 500 开始逐步往上加,观察目标端 TPS 和源端 CPU 的平衡点,一般 1000 到 2000 之间是甜点区。第二步调 JVM 堆。数据搬运用的是客户端内存,堆给太小会频繁 GC,给太大又会拖慢操作系统 IO,32G 内存的机器给-Xms4g -Xmx8g比较合理。第三步才是调threads,这个参数只在表数量多时才有效果,几十张表并发 8 个线程能明显提速,单表单跑时调这个参数没有意义。

6.3 收个尾

最后分享一个我的个人习惯:每次做迁移项目,我都会在源端和目标端各建一张sync_checkpoint表,里面记录任务名、开始位点、完成时间、校验结果。这套做法帮我复盘了不少次“为什么某张表少了几行”的问题,也让我能随时跟业务确认某个时间点的数据是否已经同步完成。

做数据库迁移同步,真正的功夫不在第一次全量跑通,而在后续每一天的增量稳定和出问题后的快速定位。dbswitch 把结构转换、全量搬运、增量追平这三件事统一在一个工具里,减轻了多工具拼接的维护成本,但工具只是底线,最终靠的是对 binlog 机制、类型映射、幂等回放这些细节的把握。希望这篇文章里的配置参数和踩坑经验能帮到你,少走一段弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询