ARTICLE DETAIL

资讯详情

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

dbswitch实战:MySQL到PostgreSQL全量迁移与增量同步

dbswitch实战:MySQL到PostgreSQL全量迁移与增量同步 简介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 要映射成timestampOracle 的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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalserewriteBatchedStatementstrue 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?stringtypeunspecified 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控制并发表数不是单表内部并发单表极大时调线程数没用得靠主键范围拆分。rewriteBatchedStatementstrue必须显式写在 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_formatROW。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 21 # 查看当前消费位点是否在推进 tail -f logs/cdc.log | grep checkpoint增量同步在大部分场景下要比全量更容易出问题因为它依赖源库日志、目标端事务和外部位点三个环节同时正常。任何一环抖动都可能导致丢数据或重复数据。所以增量任务必须有监控不能跑起来就不管。4. 完整实操用 dbswitch 把核心订单表从 MySQL 迁到 PostgreSQL4.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); -- 等 35 秒后在目标端查询 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降到 5001000read-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_formatROW这一步需要重启 MySQL 实例要在维护窗口做。修改后确认SHOW VARIABLES LIKE binlog_format返回 ROW再重新启动增量任务。如果源库是阿里云 RDS 这类托管实例控制台一般有参数组可以直接改同样需要重启实例注意提前评估对业务的影响。5.5 迁移完成后才发现字符串乱码和字符集不一致现象全量迁移结束后目标端部分中文字符显示为????或乱码增量阶段新写入的数据正常旧数据全部异常。原因源端连接串没有指定characterEncodingJDBC 默认按系统字符集读取写入 PostgreSQL 时又按库默认编码转换两边没对齐就产生了乱码。解决源端连接串显式加characterEncodingutf8目标端连接串确认client_encodingUTF8。迁移前先用一条含中文的测试数据验证编码链路不要等迁完再检查。如果已经迁移完才发现只能删除目标端数据修正连接串后重新迁移全量增量任务不受影响但全量必须重跑一遍。6. 进阶手法一致性校验与同步性能调优6.1 三段式一致性校验迁移完成后的验收我的方法是三段式校验。第一段是行数对比两张表分别COUNT(*)数量不一致直接定位漏数据。第二段是校验和对比对每张表按主键排序后拼接关键字段算一个哈希值。MySQL 里可以用CRC32PostgreSQL 里用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给太大又会拖慢操作系统 IO32G 内存的机器给-Xms4g -Xmx8g比较合理。第三步才是调threads这个参数只在表数量多时才有效果几十张表并发 8 个线程能明显提速单表单跑时调这个参数没有意义。6.3 收个尾最后分享一个我的个人习惯每次做迁移项目我都会在源端和目标端各建一张sync_checkpoint表里面记录任务名、开始位点、完成时间、校验结果。这套做法帮我复盘了不少次“为什么某张表少了几行”的问题也让我能随时跟业务确认某个时间点的数据是否已经同步完成。做数据库迁移同步真正的功夫不在第一次全量跑通而在后续每一天的增量稳定和出问题后的快速定位。dbswitch 把结构转换、全量搬运、增量追平这三件事统一在一个工具里减轻了多工具拼接的维护成本但工具只是底线最终靠的是对 binlog 机制、类型映射、幂等回放这些细节的把握。希望这篇文章里的配置参数和踩坑经验能帮到你少走一段弯路。本文还有配套的精品资源点击获取
返回列表