☰
DataX同步MySQL8实战:驱动替换、JDBC参数与类型映射避坑
2026/10/26 2:11:58 网站建设 项目流程

简介:这是一套面向使用 DataX 进行 MySQL 8.x 数据同步的开发者与数据工程师的插件包,针对 MySQL 8.x 版本提供读写支持,可用于离线数据迁移、库表同步、数据仓库入仓和日常 ETL 任务搭建,适合需要快速落地同步任务的中级开发者参考。压缩包共 36 个文件,以 32 个 jar 和 4 个 json 为主,jar 包含数据库读写插件及运行依赖,json 提供作业模板与插件描述,包体约 19.23MB,解压后可按 DataX 插件目录结构放置使用。已有 1776 人学习下载,说明其在 MySQL8 同步场景中具有一定参考价值。读者可获取可直接放入插件目录的读写组件、配套依赖和作业配置模板,并据此完成源库与目标库连接、分片并行读取、批量写入及主键冲突等参数配置,同时参考模板排查数据类型转换、并发度调整和日志异常等问题,减少自行改源码编译与依赖缺失带来的时间成本。

1. 一条 JDBC URL 耗掉一下午:datax MySQL8 到底卡在哪

有个做订单中台的同行找我,说他们 datax MySQL8 的同步作业上线第一天就挂在启动阶段,日志只有一行Unknown system variable 'query_cache_size',连是哪张表都没打出来。我第一反应不是 DataX 有问题,而是它libs目录里那个 5.1.x 的 mysql-connector 太老了——MySQL 8 把默认认证插件换成了caching_sha2_password,还把query_cache系列变量整个砍掉,老驱动连握手阶段都过不去,后面的事自然无从谈起。这份资源值钱的地方就在于它不只是一个「换个 jar」的动作,而是把驱动替换、URL 参数、类型映射、写入模式这几层一次性打平。它适合正在用 DataX 做离线同步、数据源刚升到 MySQL 8、或者要从 MySQL 8 往老版本回流数据的人。下面按我实际拆装、跑通、踩坑的顺序来。

2. 换驱动不是扔个 jar 进去:mysqlreader 与 mysqlwriter 的目录结构

2.1 先搞清楚插件在哪、谁在加载它

DataX 的插件是「一个目录一个插件」,reader 和 writer 各自独立,即使都连 MySQL,也是两套代码、两个libs。很多人只换了 reader 那一边,结果全量同步正常、回流写入报错,就是漏了 writer。

典型安装目录长这样,路径里的$DATAX_HOME是我习惯用的环境变量,实际部署时按自己的位置改:

$DATAX_HOME/ ├── bin/ │ └── datax.py # 启动入口,负责拼 java 命令 ├── conf/ │ └── core.json # 全局并发、缓冲、JVM 参数 ├── job/ │ └── mysql2mysql.json # 作业描述文件 ├── lib/ # DataX 自身的 jar └── plugin/ ├── reader/ │ └── mysqlreader/ │ ├── libs/ # 驱动 jar 就放这里 │ ├── plugin.json # 插件元信息 │ └── plugin_job_template.json └── writer/ └── mysqlwriter/ ├── libs/ ├── plugin.json └── plugin_job_template.json

判断一个插件有没有被正确加载,不用看源码,看启动日志里那几行plugin read/writer的加载打印就够了。如果换完驱动但日志里显示的仍是旧版本行为,八成是旧 jar 没删干净——libs下同时存在mysql-connector-java-5.1.47.jar和mysql-connector-java-8.0.28.jar时,JVM 的类加载顺序由文件名排序和 URLClassLoader 的扫描顺序决定,谁先被加载就是谁,这个顺序没有文档保证,属于玄学范畴,唯一的后悔药就是删干净。

位置必须存在的文件常见错误
plugin/reader/mysqlreader/libs/仅一个 8.0.x 的 connector jar新旧并存导致老驱动被优先加载
plugin/writer/mysqlwriter/libs/仅一个 8.0.x 的 connector jar只换了 reader,写入侧仍用 5.1.x
plugin/*/mysql*/plugin.jsonname与目录名一致手工改名后 name 没同步,插件找不到
$DATAX_HOME/lib/DataX 自身的依赖把 connector 放这里,reader/writer 都能看到,但排障困难

表格里最后一行值得多说一句:把驱动丢进$DATAX_HOME/lib/确实能让两个插件都用到,看起来省事,但一旦出问题,你很难判断到底是哪个插件在用它,而且升级时容易忘。我一般坚持「谁的插件谁负责」,各放各的。

2.2 换 jar 的具体操作与 plugin.json 的那一行

先把旧驱动挪走而不是直接覆盖,留个备份,出问题能立刻回滚:

# 停掉正在跑的调度,避免 jar 被占用 cd $DATAX_HOME # 备份旧驱动,注意两边的 libs 都要处理 for p in reader/mysqlreader writer/mysqlwriter; do mkdir -p /tmp/datax_bak/$(basename $p) mv plugin/$p/libs/mysql-connector-java-5.*.jar /tmp/datax_bak/$(basename $p)/ 2>/dev/null done # 放入 8.0 驱动,版本按你手上的来,别乱猜 cp ~/packages/mysql-connector-java-8.0.28.jar plugin/reader/mysqlreader/libs/ cp ~/packages/mysql-connector-java-8.0.28.jar plugin/writer/mysqlwriter/libs/ # 确认目录下只剩一个 jar ls -l plugin/reader/mysqlreader/libs/ plugin/writer/mysqlwriter/libs/

这段脚本干的三件事:备份、清理、放置。2>/dev/null是为了在目录里本来就没有 5.x 驱动时不至于让脚本中断,但执行完一定要用ls确认结果,别信脚本的静默。

接下来是plugin.json。DataX 的 MySQL 插件源码里,驱动类名是写死在常量里的,早期版本是com.mysql.jdbc.Driver。好消息是 connector 8.0.x 仍然保留了com.mysql.jdbc.Driver这个类作为兼容入口,只是加载时会打一行Loading class 'com.mysql.jdbc.Driver'. This is deprecated.的警告。所以:

  • 如果你只在日志里看到这行 deprecated 警告,功能正常,可以不管,这是最常见的状态;
  • 如果抛ClassNotFoundException: com.mysql.jdbc.Driver,说明你拿的是被裁剪过的驱动包,那就得改源码里的常量成com.mysql.cj.jdbc.Driver,重新mvn package打包,再把新 jar 覆盖回libs。

plugin.json本身一般不用动,它管的是插件名和入口类:

{ "name": "mysqlreader", "class": "com.alibaba.datax.plugin.reader.mysqlreader.MysqlReader", "description": "reader plugin for mysql", "version": "1.0.0" }

改这里只有一个场景:你手工复制了一份目录做「mysql8reader」之类的别名。那就必须保证name字段、目录名、以及 job 文件里"name"三者完全一致,任何一处不一致都会报plugin not found。我见过有人把目录改成mysqlreader8但忘了改 json,排查了两小时。

2.3 JDBC URL 上那几个不能省参数

驱动换对了,URL 写错照样连不上。MySQL 8 有三类参数必须显式给,缺一个就报错,而且报错信息往往指向性很差。

"connection": [ { "jdbcUrl": [ "jdbc:mysql://10.0.0.12:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&tinyInt1isBit=false&zeroDateTimeBehavior=convertToNull" ], "table": ["t_order"] } ]

这段 URL 里每个参数都对应过一个具体的报错,逐个说:

  • allowPublicKeyRetrieval=true:MySQL 8 的caching_sha2_password在非 SSL 连接下需要向服务端索取公钥来加密密码,驱动默认禁止这个行为,不加就报Public Key Retrieval is not allowed。这是升级后第一个撞上的坑。
  • useSSL=false:内网同步没必要走 TLS,开着反而会因为服务端证书是自签的导致握手失败,报SSLHandshakeException。生产环境如果强制要求加密,那就得配 truststore,不能简单关掉。
  • serverTimezone=Asia/Shanghai:不加这一条,8.0 驱动解析服务端返回的时区名会失败,报The server time zone value '中国标准时间' is unrecognized。别用GMT%2B8这种写法糊弄,Asia/Shanghai才能正确处理夏令时边界(虽然国内不用,但跨时区集群会用)。
  • rewriteBatchedStatements=true:写入侧的批次改写,不开的话addBatch是一条条发的,写入性能差距能到十倍级别。
  • tinyInt1isBit=false:MySQL 的tinyint(1)在 JDBC 里默认被映射成 Boolean,0/1读出来变成false/true,写回目标库就变成字符串,这条后面第 4 章还会展开。
  • zeroDateTimeBehavior=convertToNull:源库里有0000-00-00 00:00:00这种脏数据时,驱动会直接抛异常,转成 NULL 至少能让作业跑完,再单独处理。

URL 里的&在 JSON 字符串里不需要转义,但在 shell 里传参时必须加引号,否则会被 shell 当成后台执行符截断。这个坑我在写调度脚本时踩过一次,作业只跑到useUnicode=true就断了。

3. 写一份能跑通的 job.json:reader 与 writer 参数逐项对照

3.1 reader 侧:column+where还是querySql

DataX 的 mysqlreader 提供两条路:结构化配置(column+table+where)和裸 SQL(querySql)。两者互斥,同时写会报参数冲突。选哪条不是风格问题,是能力边界问题。

结构化配置的好处是 DataX 能拿到列名做类型推断,还能配合splitPk做分片并发;坏处是表达力受限,遇到需要 join、子查询、窗口函数的场景就无能为力。querySql什么都能写,但 DataX 无法解析你的 SQL 里有哪些列,只能按结果集的元数据走,而且分片能力直接失效——因为 DataX 不知道该怎么给你这段 SQL 加where id between ? and ?。

我的判断标准很简单:单表、有数值主键、数据量大,走结构化 +splitPk;多表关联、字段需要函数加工、数据量在百万以内,走querySql单并发。硬要给querySql配大 channel 数,只会让同一个 SQL 被并发执行 N 次再各自全量拉一遍,是典型的负优化。

一份完整的双端配置长这样:

{ "job": { "setting": { "speed": { "channel": 4, "byte": 1048576 }, "errorLimit": { "record": 0, "percentage": 0.02 } }, "content": [ { "reader": { "name": "mysqlreader", "parameter": { "username": "sync_r", "password": "******", "column": ["id", "order_no", "amount", "status", "created_at", "updated_at"], "splitPk": "id", "fetchSize": 1000, "connection": [ { "table": ["t_order"], "jdbcUrl": ["jdbc:mysql://10.0.0.12:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"] } ] } }, "writer": { "name": "mysqlwriter", "parameter": { "username": "sync_w", "password": "******", "writeMode": "replace", "batchSize": 1024, "column": ["id", "order_no", "amount", "status", "created_at", "updated_at"], "connection": [ { "table": ["t_order"], "jdbcUrl": "jdbc:mysql://10.0.0.21:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true" } ] } } } ] } }

几个容易被忽略的差异:reader 的jdbcUrl是数组,writer 的jdbcUrl是字符串。这不是笔误,是 DataX 的设计——reader 支持一个逻辑表对应多个物理库(分库场景),writer 只能写一个目标。写错类型会报ClassCastException,而且异常栈很深,看不出根因。

column在两端必须严格对应顺序,DataX 是按位置映射的,不是按列名。曾经有人把源端created_at和目标端create_time混着写,顺序对上了数据就"看起来正常",实际上含义全错,这类问题只有做字段级校验才能发现。

fetchSize配合 URL 上的useCursorFetch=true才能生效,作用是让驱动分批从服务端拉取结果,而不是一次性把整个结果集装进堆内存。百万级以上的表不加这个参数,OutOfMemoryError是迟早的事。

3.2 writer 侧:writeMode的三种语义差别很大

writeMode只接受insert、replace、update三个值,看起来是三种写入方式,实际语义差别能影响你的整个数据链路。

insert最直白,主键冲突直接报Duplicate entry,配合errorLimit.record: 0就是让作业失败。适合目标表是空的、或者你有把握数据绝对不重复的初始化场景。

replace底层是REPLACE INTO,MySQL 的实现是「先 DELETE 再 INSERT」。这意味着三件事:目标表上的 DELETE 触发器会被触发、自增主键会被消耗、没在column里列出的字段会被重置成默认值。如果目标表有几个由数据库默认值填充的字段(比如update_time DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP),replace之后这些字段的行为和你预期的不一样。

update是按主键更新,需要目标表有主键且column里包含主键列。它的局限是不做插入,源端新增的记录会被直接丢掉。

真要做「有则更新、无则插入」,DataX 原生不支持 upsert。常见做法是分两步:先跑一遍writeMode: insert但把errorLimit.record设成一个较大值吞掉主键冲突,再跑一遍update。或者干脆在目标端用临时表 +INSERT ... ON DUPLICATE KEY UPDATE自己写 SQL 处理。这两种方案我都用过,前者省事但脏数据统计不准,后者可控但多一次落表开销。

batchSize默认 1024,配合rewriteBatchedStatements=true才有意义。调大到 4096 或 8192 通常还能再快一些,但要注意单批数据量乘以 channel 数不能超过目标库的max_allowed_packet,否则会报Packet for query is too large。

3.3splitPk与channel的关系:不是越大越快

splitPk指定分片字段后,DataX 会先执行SELECT MIN(pk), MAX(pk) FROM table,然后按 channel 数把区间切成若干段,每个 task 认领一段。所以有几个硬约束:

分片字段必须是数值型或能比较的整型,字符串主键(比如 UUID)不行,会报类型转换错误。表里必须有数据,空表的MIN/MAX返回 NULL,DataX 会退化成单 task。分片字段的分布要均匀,如果主键是雪花 ID 且写入有明显的时间倾斜,切出来的段数据量会严重不均,某个 channel 跑完了其他还在跑,整体耗时由最慢的那个决定。

channel的取值我一般按min(表数量 × 2, CPU 核数)来估,再根据目标库的写入能力往下压。源库能扛住 16 并发不代表目标库能,写入端的瓶颈通常在索引维护和锁竞争上。曾经有一次把 channel 从 4 调到 16,reader 侧耗时降了 60%,但 writer 侧因为唯一索引冲突重试,整体反而慢了。这个教训之后,我调并发都是读写两端分开测,先定 writer 的上限,再倒推 reader。

errorLimit里的record和percentage是「或」的关系,任一超限就终止。生产环境我倾向把record设成 0,percentage设成 0.001 这种极小值,让问题尽早暴露,而不是让作业带着脏数据跑完再回头查。

4. MySQL 8 带来的类型与语法变化:从tinyint(1)到保留字

4.1 保留字、排序规则和默认值

MySQL 8 新增了一批保留字,其中好几个在业务表里极其常见:rank、groups、system、window、over、recursive、lateral、except。这些词在 5.7 里可以裸用做列名,8.0 里不加反引号就是语法错误。

问题在于,DataX 拼 SQL 时对列名的处理取决于你的配置方式。走column结构化配置,DataX 会自己加反引号,一般没事;走querySql写裸 SQL,那就是你自己负责,SELECT rank FROM t在 8.0 上直接报错。我一般的做法是:只要目标库是 8.0,querySql里所有列名统一加反引号,不省这个事。

排序规则的变化更隐蔽。8.0 的默认字符集是utf8mb4,默认排序规则是utf8mb4_0900_ai_ci。DataX 本身只搬数据不搬 DDL,所以同步过程不受影响,但如果你的下游是 5.7 或者更早的版本,建表时用了utf8mb4_0900_ai_ci就会报Unknown collation。跨版本同步时,目标端的建表语句必须手工改成utf8mb4_general_ci。

还有一个explicit_defaults_for_timestamp参数,8.0 默认开启,5.7 默认关闭。开启后,timestamp列不再自动获得NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP的隐式行为。源端 5.7 建的表同步到 8.0 环境,如果建表脚本是从源端直接导出的,行为会不一致。

4.2 类型映射表:哪些字段会在搬运中变形

DataX 内部只有六种类型:LONG、DOUBLE、STRING、BOOLEAN、BYTES、DATE。MySQL 的几十种类型都要映射到这六种上,映射过程有损,下面这些是实际出过问题的:

MySQL 类型DataX 内部类型典型问题处理方式
tinyint(1)BOOLEAN0/1变false/trueURL 加tinyInt1isBit=false
bit(1)BYTES写出时变成二进制串目标端改为tinyint或显式转换
decimal(65,30)STRING大精度场景精度丢失目标端保持同精度,别中途转 double
datetime(6)DATE毫秒被截断两端列定义一致,且都用datetime(6)
jsonSTRING键顺序可能变化下游按 JSON 解析,别做字符串比对
enumSTRING枚举值越界报错目标端用varchar承接
timestampDATE时区偏移导致差 8 小时统一serverTimezone,或改datetime

timestamp那一行值得单独说。timestamp在 MySQL 内部存的是 UTC,读写时按会话时区转换;datetime存的是字面值,不做转换。如果源端连接时区是+08:00,目标端连接时区是 UTC,同一个timestamp值搬过去就会差 8 小时。这不是 DataX 的 bug,是timestamp类型本身的语义。跨时区同步我一般要求两端都用datetime,从根上绕开这个问题。

decimal转STRING是 DataX 的保守选择,好处是不丢精度,坏处是如果中间有人做了CAST或者下游按数值处理,会引入浮点误差。金额字段尤其要注意,两端列定义必须完全一致,别一边decimal(18,2)一边decimal(18,4)。

5. 排查记录:六个把我按在工位上的问题

5.1 连接与驱动层

现象一:作业启动三秒即失败,日志报Unknown system variable 'query_cache_size'。原因:libs下的驱动仍是 5.1.x,这个版本的驱动在初始化连接时会执行SHOW VARIABLES LIKE 'query_cache_size',而 MySQL 8.0 已经移除了查询缓存。解决:按第 2 章的方法换成 8.0.x 驱动,并且两个插件目录都要换,只换 reader 的话写入侧还会在同一个地方挂。

现象二:报Public Key Retrieval is not allowed,但驱动版本确认是 8.0。原因:MySQL 8 默认认证插件是caching_sha2_password,非 SSL 连接下驱动需要向服务端索取 RSA 公钥来加密传输密码,出于安全考虑默认禁止。解决:URL 加allowPublicKeyRetrieval=true。如果安全策略不允许,另一条路是给同步账号改成mysql_native_password插件,ALTER USER 'sync_r'@'%' IDENTIFIED WITH mysql_native_password BY '...',但这属于降级,不推荐。

现象三:报The server time zone value '中国标准时间' is unrecognized。原因:8.0 驱动对服务端返回的时区名称解析更严格,中文时区名不再被识别。解决:URL 加serverTimezone=Asia/Shanghai。注意别写成GMT+8,+在 URL 里要编码,而且GMT+8不带夏令时规则,跨时区场景会出错。

现象四:跑十几分钟后连接断开,报Communicating link failure。原因:MySQL 的wait_timeout默认 8 小时,但中间有防火墙或负载均衡时,空闲连接可能在几分钟内就被回收,驱动侧并不知道。解决:缩短单批处理时间(降低batchSize、减小splitPk分片粒度),或者把wait_timeout调到远大于单批耗时。依赖autoReconnect=true不可靠,它在事务中间重连会导致数据不一致,我不建议开。

5.2 数据与语义层

现象五:作业成功结束,但目标表数据量比源表多了几十条。原因:writeMode用了insert,而源端在同步窗口内有主键更新——同一主键被读到了两次(分片边界重复),第二次插入时因为errorLimit.record设得宽松被吞掉了报错,但实际写入了重复行。解决:splitPk的分片区间要保证左闭右开且互不重叠;errorLimit.record在生产环境设 0,让问题立刻暴露;写完做一次主键去重校验。

现象六:中文全部变成问号,但两端表都是utf8mb4。原因:URL 里的characterEncoding=utf8在 8.0 驱动里实际指向utf8mb3,三字节编码,遇到 emoji 或者生僻字就变成?。解决:改成characterEncoding=utf8保留(兼容性更好)+ 显式加connectionCollation=utf8mb4_0900_ai_ci,或者直接把characterEncoding写成utf8mb4。改完后必须重新跑一遍全量,已经写坏的数据不会自己修好。

这六条里,前三条几乎每个升级 MySQL 8 的团队都会撞上一遍,后三条则是数据层面的静默失败——不报错,但结果是错的,危害更大。我的习惯是每次改完配置先跑一张小表,用SELECT COUNT(*)和主键MAX(id)两头对,确认无误再上大表。

6. 进阶:用指纹校验兜住静默失败

配置调通只是第一步,真正决定这份资源能不能长期用的是校验环节。我一般不看 DataX 自己的任务日志(它只说写了多少条,不说写得对不对),而是单独跑一段指纹比对。

import pymysql def fingerprint(host, port, user, password, db, table, cols): conn = pymysql.connect( host=host, port=port, user=user, password=password, database=db, charset="utf8mb4", connect_timeout=10 ) try: # 把所有列拼成一个字符串,逐行算 CRC32,再求和 expr = "CONCAT_WS('#', " + ", ".join(["IFNULL(`%s`, '')" % c for c in cols]) + ")" sql = "SELECT COUNT(*) AS n, IFNULL(SUM(CRC32(%s)), 0) AS s FROM `%s`" % (expr, table) with conn.cursor() as cur: cur.execute(sql) n, s = cur.fetchone() return {"rows": n, "checksum": s} finally: conn.close() if __name__ == "__main__": cols = ["id", "order_no", "amount", "status", "created_at", "updated_at"] src = fingerprint("10.0.0.12", 3306, "sync_r", "***", "order_db", "t_order", cols) dst = fingerprint("10.0.0.21", 3306, "sync_r", "***", "order_db", "t_order", cols) print("source:", src) print("target:", dst) print("match:", src == dst)

逻辑上做了两件事:行数比对和内容指纹比对。CONCAT_WS用#做分隔符把一行拼成字符串,IFNULL处理 NULL 保证拼接不返回 NULL,CRC32逐行算校验值后SUM聚合,这样一次查询就能拿到整表指纹,不用把数据拉到本地。

参数上有几个取舍要说清楚。分隔符必须选一个业务数据里绝对不会出现的字符,#比较安全,但如果某个字段可能含#,就得换\u0001这类控制字符。CRC32是 32 位,理论上存在碰撞,对于千万级以上的表,我建议换成MD5聚合(SUM(CONV(SUBSTR(MD5(expr),1,8),16,10))),代价是 CPU 开销翻几倍。列清单在两端必须完全一致且顺序一致,否则指纹永远对不上。

这段脚本我只在同步完成后跑一次,全表扫两遍,大表上会锁住一部分 IO。真实环境里我会把它放在从库上执行,或者按主键分片跑,避免影响线上。

增量同步则靠时间水位。DataX 本身不解析变量,我的做法是用脚本渲染 job 模板,把水位写进where:

# gen_job.py 负责把模板里的 __START__ / __END__ 替换成实际时间 python3 gen_job.py --start "2024-05-01 00:00:00" --end "2024-05-02 00:00:00" > /tmp/job_inc.json python $DATAX_HOME/bin/datax.py /tmp/job_inc.json

调datax.py这里用的是python而不是python3,因为不少发行版里的datax.py对python3的兼容处理得并不好,会在参数解析阶段就报TypeError。如果环境里只有python3,直接绕过脚本,用java -server -Xms4g -Xmx4g -classpath "$DATAX_HOME/lib/*" com.alibaba.datax.core.Engine -mode standalone -jobid -1 -job /tmp/job_inc.json起,效果一样,还省掉一层解释器。

水位字段必须是有索引的updated_at,而且水位窗口要有重叠——比如上一批的结束时间作为下一批的开始时间再往前推 5 分钟。因为事务提交顺序和updated_at写入顺序不一致,边界上的记录很容易漏。重叠带来的是重复,重复可以靠replace或update幂等消化,漏了就再也补不回来了。

从那以后我每次配新的同步链路,都强制先跑「小表全量 + 指纹比对 + 边界时间增量」这三步,跑通了再放量。多花二十分钟,比事后从几千万行里捞几十条错数据划算得多。希望帮到你。

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

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

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

立即咨询