在一家同时跑着 Oracle 和 MySQL 的公司里做数据平台,最常遇到的场景就是:核心业务系统都在 Oracle 上,但后建的 BI 报表、经营分析、数据中台只认 MySQL。两边要定期同步数据,这活儿听起来简单,真做起来却有一堆讲究。
我最开始是让开发写存储过程,通过 dblink 往 MySQL 插数。结果每次同步都心惊胆战:源库 CPU 被拉高,目标库写入一慢就堵事务,不同数据库之间的类型还得手工转换,字段一多配置就乱。后来换了 DataX,再把同步任务统一放到 DataX-Web 上调度管理,才算是把这块彻底理顺。这篇就完整记录一次 Oracle 到 MySQL 的实战配置。重点不是教你怎么点按钮,而是把 DataX 里最核心的四个概念——并发通道(Channel)、分片字段(splitPk)、TaskGroup(任务组)、Task(子任务)——用一条真实同步配置串起来讲明白。适合刚开始接触 DataX、想把批量同步任务做得又快又稳的读者参考。
1. 先说清楚业务场景:Oracle 主库为什么非得往 MySQL 同步
很多公司并不是一开始就想搞两套数据库,而是业务发展过程中自然长成的。我们这边的情况是:订单、客户、商品这些核心数据都在 Oracle 里,ERP、CRM、WMS 各种系统都依赖它,不能随便动。但近几年上的数据分析平台、可视化报表、甚至是一些外包团队开发的新系统,都是基于 MySQL 的。新系统不可能直连 Oracle 生产库,只能靠周期性同步拿数据。
1.1 手工脚本同步的痛点在哪里
最早我们用的是 dblink 加存储过程,每天晚上跑批。这种方式在小数据量下没什么问题,但数据量一旦上来,问题就非常明显。
一个是数据库负载不可控。dblink 方式本质上是把源库当成了一个普通客户端,存储过程里如果写了全表扫描逻辑,Oracle 的 CPU 和 IO 会瞬间飙高,影响白天还在跑的业务。另一个是数据一致性不好保证。跑批过程中网络抖动、目标表锁冲突、字段超长,任何一个环节出错,整个存储过程就中断了,第二天发现报表数据是残缺的,还得人工对账重跑。
更麻烦的是类型转换。Oracle 的 NUMBER、VARCHAR2、DATE 转到 MySQL 的 int、varchar、datetime,不是简单的一一对应。一开始我们靠开发在 SQL 里手工写 TO_CHAR、TO_NUMBER,字段少还能应付,字段上百个以后,维护成本高到让人崩溃。
1.2 为什么挑 DataX 而不是其他同步工具
当时也考察过其他方案,比如基于日志的 CDC 工具、商业 ETL 工具,甚至直接用 Kettle 拖拽。最后选 DataX 主要是看中几点。
DataX 是阿里开源的异构数据源同步框架,插件化设计,Oracle 和 MySQL 的 reader、writer 都是现成的,改改 JSON 就能用,不需要写一行业务代码。它对源端是普通的 JDBC 查询方式,不依赖数据库日志,也就不会对 Oracle 的归档模式、日志解析权限提出额外要求,这一点在生产库上很重要。还有一个原因是它的并发模型设计得比较直观,一个 channel 就是一条数据通道,想提速加通道数就行,出问题容易排查。
当时也担心过 DataX 社区维护节奏的问题,但这么多年用下来,发现同步任务这种场景对版本迭代并不敏感,稳定才是第一位的,所以一直用到现在。
1.3 DataX-Web 在 DataX 之上补了什么
单纯用 DataX 命令行走任务,问题在于没有统一的管理界面。任务分布在各个服务器上,跑没跑成功全靠人肉盯日志,调度只能靠 crontab,谁改过任务配置也无从追溯。
DataX-Web 是一个基于 DataX 的可视化调度管理平台。它把 DataX 的 Job JSON 封装成了任务模板,再通过任务绑定执行器的方式,把任务分发到指定机器上执行。页面里能看到每个任务的历史执行记录、日志、运行状态,还能配 cron 表达式做周期性调度。对我来说,它最大的价值不是省了敲命令那几秒钟,而是让整个同步体系从“脚本散落”变成了“平台统一”。
2. 环境准备与第一个同步任务的完整发布流程
现在开始进入实操。假设你已经有两套环境:Oracle 19c 在 192.168.10.20,MySQL 8.0 在 192.168.10.30,网络互通,内网同步。下面这套流程我按自己的部署习惯写,版本不同界面可能有细微差别,但核心思路一致。
2.1 环境安装:两台机器需要准备什么
DataX 本体和 DataX-Web 可以部署在同一台机器上,也可以分开。我建议把 DataX 装到离源库网络比较近的一台单独服务器上,别和业务应用混跑,同步任务对带宽和 IO 的占用不小。
安装 DataX 本体很简单:下载 DataX 安装包,解压后目录里有 bin、plugin、lib 等几个子目录。核心的可执行脚本在bin/datax.py。我们需要确认 Python 环境可用,且本机已经装了 JDK 8 及以上版本。DataX 本身是 Java 写的,JDK 版本太老或太新都可能踩坑。
DataX-Web 我用的是 GitHub 上开源的版本,它分为两个部分:前端 Web 页面和执行器(executor)。前端负责配置任务、查看日志;执行器负责真正拉起 DataX 进程。部署时先把 DataX-Web 的后端服务启动起来,再配置执行器地址,让前端能连接到执行器机器。
有一点容易被忽略:DataX-Web 的调度中心和执行器最好都配置了时区,否则定时任务的执行时间和服务器本地时间对不上。我们线上就出过一次任务提前一小时跑的故障,查了半天发现是容器时区默认 UTC 导致的。
2.2 DataX-Web 里创建一个同步任务的入口
在 DataX-Web 页面上,正常流程是:
- 先创建项目,一般一个项目对应一个业务线。
- 进入“任务管理”模块,创建任务模板,把 DataX 的 JSON 配置粘贴进去。
- 创建任务,选择刚才的模板,绑定执行器和调度表达式。
- 如果想立即验证数据,可以手动触发运行,先不配调度。
这里要分清楚“任务模板”和“任务”的关系。模板是静态配置,相当于一类任务的公共定义;任务才是调度实体,绑定执行器后才有实际的运行记录。同一个模板可以创建多个任务,比如不同日期的增量同步,通过任务参数区分。
2.3 一个可直接复制的 Oracle→MySQL 任务 JSON
下面是核心部分,先看一个完整的、可用的 JSON 配置。
{ "job": { "setting": { "speed": { "channel": 4, "byte": -1, "record": -1 }, "errorLimit": { "record": 0, "percentage": 0.02 } }, "content": [ { "reader": { "name": "oraclereader", "parameter": { "username": "report_ro", "password": "your_password", "connection": [ { "table": ["orders"], "jdbcUrl": ["jdbc:oracle:thin:@//192.168.10.20:1521/orcl"] } ], "column": [ "ID", "ORDER_NO", "AMOUNT", "CREATE_TIME" ], "splitPk": "ID", "where": "CREATE_TIME >= to_date('2024-08-01', 'yyyy-mm-dd')" } }, "writer": { "name": "mysqlwriter", "parameter": { "username": "report", "password": "your_password", "connection": [ { "table": ["orders"], "jdbcUrl": ["jdbc:mysql://192.168.10.30:3306/report_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true"] } ], "column": [ "ID", "ORDER_NO", "AMOUNT", "CREATE_TIME" ], "preSql": ["truncate table orders"], "writeMode": "insert", "batchSize": 2048 } } } ] } }这个配置的含义很直白:reader 从 Oracle 的 orders 表里读 ID、ORDER_NO、AMOUNT、CREATE_TIME 四列,只取 2024-08-01 之后的数据;writer 把数据写入 MySQL 的 orders 表。写入前先清空目标表,适合全量刷新场景。
重点先看两个参数:splitPk和speed.channel。这两项直接关系到并行度,后面会专门展开。byte和record都设成 -1,表示不限流,不走字节数或记录数限制,优先保证速度。如果担心源库压力,可以给 byte 设一个值,比如 10MB/s,DataX 会按这个速度限流。
2.4 为什么 Oracle Reader 要加 where 条件
很多新手做全量同步时会在 Oracle 源端直接整表查询。表小的时候无所谓,表一大就很伤:DataX 虽然会做并发分片,但查询条件没有下推到数据库,Oracle 可能需要做全表扫描,共享池和 IO 都会被打满。
所以我习惯在 where 里先做粗过滤,把需要的日期范围或状态范围写清楚。这既能让 Oracle 走索引,也能减少网络传输的数据量。如果表有主键或者时间索引,这个 where 条件的收益非常明显。
3. 并发通道、分片字段、TaskGroup、Task 到底是怎么配合的
这是整篇的核心,也是很多人在 DataX 文档里看半天没看明白的地方。我用大白话把这四个概念拆开讲。
3.1 一个 Job 是如何被拆成多个 Task 的
DataX 里,一个同步任务整体称为一个 Job,也就是你在 DataX-Web 里配置的那份 JSON。Job 在运行时会经历两个阶段:拆分(split)阶段和执行阶段。
在拆分阶段,DataX 会调用 reader 的 split 方法,把整体的数据读取任务切成若干份,每一份就是一个 Task。Task 是最小的执行单元,它负责读取一段数据并写入目标端,内部是单线程的。如果你不配置任何分片逻辑,那 Job 只有一个 Task,全表数据由这一个 Task 串行搬运,速度自然上不去。
这个拆分动作和两个因素有关:一个是并发通道数,一个是分片字段。它们共同决定了 Task 的数量和边界。
3.2 splitPk 分片字段是怎么切数据分片的
splitPk指定一个字段,DataX 会用它把查询结果切分成多个区间。比如 orders 表的主键 ID 是从 1 到 500 万的连续自增整数,channel 配成 4,DataX 就会把数据大致均分成四个区间,例如 1~125 万、125 万~250 万、250 万~375 万、375 万~500 万,生成四个 Task。
实际执行时,每个 Task 会带着一段区间条件去查询 Oracle,相当于在 reader 的 SQL 上自动追加了类似WHERE ID >= ? AND ID < ?的条件。这样每个分片走主键或索引的范围扫描,对源库更友好,数据也能并行读取。
反过来,如果你不配置 splitPk,哪怕 channel 配了 8,DataX 也只能生成一个 Task。因为 reader 没法把数据切成多份,并发配置就形同虚设。这也是我见过最多的“配了并发没提速”的原因。
3.3 channel 并发通道控制的到底是什么
channel 中文叫通道或者管道,可以理解成一个并发线程。每个 channel 内部维护着 reader 和 writer 两端的连接,一个 channel 同时只能执行一个 Task。
你在配置里把speed.channel从 1 改成 4,本质上是告诉 DataX:同一时刻最多可以有 4 个 Task 并行执行。这 4 个 Task 会各自从 Oracle 读数据并往 MySQL 写数据。所以通道数越高,同一时刻在跑的数据库连接数、网络连接数、内存占用也会跟着升高。
3.4 TaskGroup 任务组是怎么把 Task 组织起来的
TaskGroup 是很多人最容易混淆的概念。官方文档和各类文章里一会儿说 Task 一会儿说 TaskGroup,搞得人云里雾里。
在 DataX 的运行框架里,Job 拆分出来的 N 个 Task 并不会一下子全部交给 N 个线程去跑,而是先被分到若干个 TaskGroup 中。TaskGroup 相当于一个任务容器或者调度组,组内维护着自己的 channel,负责从中取出 Task 并分发给线程执行。
你可以这样理解:Task 是“干活的单元”,channel 是“干活的人”,TaskGroup 是“干活的小组”。公司里有个大项目,被拆成了 20 个小任务,不可能 20 个人同时开工,人太多了协调不过来,所以分成了 4 个小组,每组 5 个人,每个小组认领 5 个小任务,谁干完谁去接下一个。
TaskGroup 存在的意义正是为了解决这种调度和管理问题。如果 Task 数量很大,比如几百上千个,直接全部并发会产生大量连接和线程,机器扛不住。通过 TaskGroup 分层调度,DataX 可以有节奏地把任务分配给各个通道去消费。
这里要特别提醒一下:DataX-Web 页面上也有一项叫“任务组管理”,但它和 DataX 内部的 TaskGroup 是两码事。DataX-Web 的任务组是用来给执行器分组的,比如你有两台执行器,可以分成两个任务组,分别调度不同的任务;DataX 的 TaskGroup 是运行时框架里的概念。别把这两者混了,坑过不少人。
3.5 四个概念放在同一个案例里看
还是拿上面的 orders 表举例。假设 channel=4,splitPk=ID,总数据约 500 万行。
- Job 是整个同步任务,负责拆解和调度。
- split 阶段基于 ID 把数据切成 4 份,生成 4 个 Task。
- 运行时框架把 Task 分配给 TaskGroup 管理,TaskGroup 里的 channel 按并发度去执行 Task。
- 每个 Task 负责一段 ID 区间的数据,单线程完成读取和写入。
如果在日志里看到“task 0、task 1、task 2、task 3”这样的编号,不用好奇,那就是 4 个并行 Task。再看机器上的数据库连接数,大概率也是 4 组左右的读写连接。
4. 并发参数到底怎么调:一次真实压测记录
理论讲完,必须用数据说话。下面是我环境里一次实际压测的记录,供你参考调参思路,不建议直接照搬数字,因为不同表结构、数据库硬件、网络环境差异很大。
4.1 压测环境与固定条件
源端 Oracle 19c,16 核 32G,orders 表约 500 万行,单行平均 1KB,数据总量大约 5GB。目标端 MySQL 8.0,8 核 16G。中间是千兆内网。分片字段统一用 ID 主键,writer 端 batchSize 固定 2048,不限制流量。
每个并发档位我都连续跑两次取平均值,避免第一次运行时有连接初始化等冷启动影响。表结构固定,目标表每次都先 truncate 再写入,保证口径一致。
4.2 不同 channel 数下的实测对比
结果如下表:
| channel 数 | 总耗时 | 平均同步速度 | 源库 CPU 峰值 | 表现评述 |
|---|---|---|---|---|
| 1 | 18 分 30 秒 | 约 4.6 MB/s | 15% | 稳定但太慢,单通道是保底方案 |
| 2 | 10 分 05 秒 | 约 8.5 MB/s | 28% | 速度明显提升,源库压力可控 |
| 4 | 6 分 12 秒 | 约 13.7 MB/s | 55% | 速度与压力比较平衡 |
| 8 | 4 分 08 秒 | 约 20.1 MB/s | 80% | 速度最快,源库负载已偏高 |
| 16 | 5 分 40 秒 | 约 15.2 MB/s | 95% | 速度反而下降,源库成了瓶颈 |
这个结果很典型:通道数从 1 加到 8,速度接近线性提升;但从 8 加到 16,不仅没有继续加速,总耗时反而变长了。原因是当 16 个并发 Task 同时在 Oracle 上做范围扫描时,数据库的 CPU、IO 都被打满,查询产生大量等待,单条 SQL 变慢,整体吞吐量反而掉下来。
4.3 怎么找到“当前环境的最优并发”
根据我的经验,最优并发不会出现在某个固定数值上,而是要综合看三条线。
第一条线是源库负载。Oracle 的 CPU 使用率尽量控制在 70% 以下,给业务留出余量。这里采集的是同步期间的峰值,如果峰值长期超过 80%,我建议降一档并发。
第二条线是目标库写入能力。MySQL 写入瓶颈往往不在 SQL 本身,而在磁盘 IO 和 binlog 落盘。可以用iostat、dstat看目标库的磁盘利用率,如果%util长期接近 100%,再往上加并发只是把压力转移到了 MySQL。
第三条线是网络带宽。千兆内网理论上有 100MB/s 左右,但实际能够稳定跑到 50MB/s 就非常不错了。如果你看到总流量已经逼近网卡上限,并发加得再多也没有意义。
所以实操时我的方法是:从 channel=2 开始跑,往上翻倍试探,每档看一次源库 CPU 峰值和总体耗时,当耗时不再下降或源库负载超过 75% 时,往回退一档,再用这一档长期跑。大多数表在 channel=4 到 channel=8 之间就能获得不错的性价比。
5. 实战最容易踩的坑:Oracle 源端和 MySQL 目标端的边界条件
配置跑通只是第一步。真正让人头疼的是那些“能跑但结果不对”或者“跑到一半报错”的情况。下面这些坑我基本都踩过,每一项都值得你在上线前检查一遍。
5.1 splitPk 选择不当导致的漏数据和数据倾斜
分片字段不是随便挑一个字段就行。最理想的是数值型主键,值单调递增、分布均匀、非空。如果选了分布不均匀的字段,比如状态字段,只有 0 和 1 两个值,DataX 切分出来的区间就只有一个能查到大量数据,其他区间几乎为空,最终还是相当于单线程跑,并发完全失效。
还有一个很容易踩的坑是分片字段存在 NULL。DataX 在做区间切分时,对 NULL 值的处理并不友好,可能出现边界条件漏数据,甚至直接报错。所以我一般建议,如果你的表在分片字段上有 NULL,要么在 SQL 里用NVL(ID, 0)处理,要么换一个非空字段。有人会说时间字段能不能作为分片字段,我的答案是能用但慎用。时间字段如果重复值多,或者时区不一致,切分后可能产生区间重叠和数据漏读。线上环境我一般只用 ID 或唯一数字序列做分片,靠谱得多。
5.2 Oracle 和 MySQL 类型映射的坑
两边数据库类型体系差异比想象中大,通常不是 DataX 本身的问题,而是建表结构没对齐。下表是我整理的核心映射参考:
| Oracle 类型 | MySQL 推荐类型 | 注意事项 |
|---|---|---|
| NUMBER(18) | BIGINT | 超过 2^31 别用 INT |
| NUMBER(18,2) | DECIMAL(18,2) | 映射成 INT 会丢精度 |
| VARCHAR2(2000) | VARCHAR(2000) 或 TEXT | MySQL 的 varchar 有长度上限,超了要建 text |
| DATE | DATETIME | Oracle date 带时分秒,MySQL date 不带,务必用 datetime |
| TIMESTAMP | DATETIME | 注意时区传参,jdbcUrl 里加 serverTimezone |
| CLOB | LONGTEXT | 大文本字段要做字符集校验 |
| BLOB | LONGBLOB | 二进制字段看业务是否必须同步 |
最容易中招的是 NUMBER 精度过大。Oracle 里NUMBER(38,0)如果直接映射到 MySQLINT,数值一大就会报Out of range value for column,整条数据写入失败。我在一次渠道订单同步中就因为这个字段导致不少订单缺失,最后靠 errorLimit 里的脏数据记录才定位到。建目标表时宁可字段宽一点,也不要刚好卡边界。
5.3 字符集、时区、批量写入的边界
Oracle 字符集是 ZHS16GBK,MySQL 可能建表时用的 utf8mb4,两边不一致时最容易出现乱码或者是“字符串截断”报错。DataX 的 JDBC URL 上要明确字符集参数,Oracle 端一般通过oracle.jdbc.defaultNChar或连接属性做控制,MySQL 端在 jdbcUrl 里加上characterEncoding=utf8mb4。记住一个原则:连接串上的字符集一定要明确写出来,别用数据库默认值。
时区问题主要在 Oracle 的TIMESTAMP WITH TIME ZONE类型。同步到 MySQL 的 datetime 字段时,如果两边时区不同,时间数据会被 JVM 默认时区影响,多出 8 个小时或者少 8 个小时。我建议在启动 DataX 的 JVM 参数里统一指定时区,比如-Duser.timezone=Asia/Shanghai,别依赖操作系统默认时区。
批量写入也有讲究。MySQL Writer 的batchSize默认值是 1024,设大一点能明显提升写入效率,但也不是越大越好。我实测 2048 到 4096 是比较合适的区间,再大容易撑爆 MySQL 的 max_allowed_packet,出现Packet for query is too large报错。批量写入还建议在 jdbcUrl 上加rewriteBatchedStatements=true,这个参数能让 JDBC 把多条 insert 合并提交,性能提升非常可观。
5.4 常见报错与排查方向汇总
遇到报错先不要急,按下面的表格对照排查:
| 报错特征 | 大概率原因 | 排查动作 |
|---|---|---|
| ORA-12899 value too large | Oracle 源字段长于 MySQL 目标字段 | 检查两边字段长度定义,加宽目标表 |
| Out of range value for column | NUMBER 精度映射错误 | 目标列改成 DECIMAL 或 BIGINT |
| Packet for query is too large | batchSize 太大或字段内容超大 | 调小 batchSize,检查是否有超长文本 |
| Communications link failure | 网络抖动或数据库连接被断开 | 检查网络稳定性,适当降低并发 |
| 乱码 | 字符集不一致 | 统一 JDBC 连接串字符集参数 |
| 同步缺失部分数据 | splitPk 有 NULL 或分片不均匀 | 检查分片字段空值,换主键字段 |
DataX 日志里报错信息一般会带上具体插件和行号,比如oraclereader-0表示第 0 个并发分片。如果 errorLimit 配置允许了脏数据比例,DataX 会把失败记录写到日志里,但业务表数据已经缺失,所以生产环境我一般把record设成 0,宁可任务失败也不允许静默丢数。
6. 从“跑通一次”到“长期稳定同步”的运维习惯
最后一个环节是让任务活下来,而不是跑一次就完事。这部分更多是运维经验,数据同步的稳定性问题几乎都集中在调度和监控上。
6.1 DataX-Web 里的调度与执行日志
DataX-Web 支持 cron 表达式调度。我的标准配法是:全量同步放凌晨业务低峰,cron 表达式写清楚分钟和小时,比如0 2 * * *表示每天凌晨 2 点。增量同步则根据业务时效要求调整频率,有的半小时一次,有的一天一次。
任务运行后,一定要养成看执行日志的习惯。DataX 每次任务结束都会打印“任务启动时刻”、“任务结束时刻”、“任务总计耗时”,还会输出平均流量和错误记录数。如果平均流量明显低于历史水平,即使任务显示成功,我也建议去查一下,看是不是源库加了慢查询或者网络出了问题。
日志里另外一个有用信息是脏数据统计。DataX 会明确打印脏数据条数和占比,如果百分比不断上升,说明源端或目标端的数据质量出现了变化,早发现早处理。
6.2 增量同步怎么设计才稳健
全量同步用truncate + insert没问题,但对大表来说每次全量成本太高。到了后期,我更推荐用增量同步来做日常更新。
增量同步的核心思路是加一个时间水位线。在 DataX-Web 的任务参数里,每次调度时传入一个业务日期或者上次同步时间,然后在 reader 的 where 条件里用它过滤数据。
"where": "CREATE_TIME >= to_date('${lastSyncTime}', 'yyyy-mm-dd hh24:mi:ss')"任务参数通过 DataX-Web 的运行参数传入,类似-DlastSyncTime=2024-08-01 00:00:00。上次同步时间可以查目标表里的 max(CREATE_TIME),也可以在调度外层用一个状态位维护。增量字段建议选择有索引的时间列,否则每次增量查询全表扫一遍,代价比全量还大。
增量同步最怕的是时间字段回拨或者数据补录。业务人员手工改了一条历史订单的时间,会导致这条数据不在增量窗口内。所以很多团队会保留每周一次全量作为兜底,配合每日增量,这个组合在工程上非常实用。
6.3 我在实际维护中总结的几条经验
同步任务谁都会配,但能稳定跑半年不出问题,靠的是细节。
第一,DataX 所在服务器的磁盘空间要盯紧。DataX 本身不落业务数据,但日志文件会一直增长。DataX-Web 的执行器日志如果不定期清理,几个月就能吃掉几个 GB。我在日志目录上挂了定时清理任务,只保留最近 30 天。
第二,不要把执行器跟其他重负载应用混布。同步任务吃 IO 和 CPU,和数据量大的在线服务放在一起,两边都不痛快。独立机器、独立资源是最省心的。
第三,任何配置改动都要先在测试表上跑一遍。DataX 的 JSON 配置看着简单,但一个括号写错就会导致任务解析失败,如果直接在线上改,又赶上没人在场,问题只能等到次日数据对账时才发现。
最后一个小技巧:在 DataX-Web 的任务名称上,把业务含义、同步周期、目标表名写清楚,比如“orders全量同步_每日凌晨2点_写入orders报表库”。听起来很简单,但当你维护几十个任务时,一个清晰的任务命名能节省大量排查时间。数据同步这件事本身不难,难的是把每一步都做得规范,让系统在没有人工干预的情况下也能按预期运转。