简介:面向使用 DataX 在 MySQL 8.x 与异构数据源之间进行批量同步的开发者、数据工程师和运维人员,这套插件包针对官方插件在 MySQL 8 驱动适配、读写链路搭建与依赖补齐上的常见痛点,提供可集成到 DataX 插件体系中的 reader 与 writer 实现,适合中高级数据集成与迁移场景。压缩包共 36 个文件,以 32 个 jar 和 4 个 json 为主,大小约 19.23MB;jar 涵盖读写插件本体、公共依赖、数据库连接驱动及日志组件,json 对应插件描述与作业模板,结构清晰,便于排查依赖和按任务复用。目前已有 1776 人学习下载。资源围绕 mysqlreader 与 mysqlwriter 组织,覆盖连接参数、批量写入、主键冲突处理、字段类型转换、并发调优和错误日志定位等实践内容,可用于搭建稳定高效的 MySQL 8 数据迁移链路,也适合定位驱动兼容性、依赖缺失和同步性能问题。
1. job.json 改了、jar 也换了,为什么还是连不上 MySQL 8
job.json 里 reader 的 name 已经指向新目录,libs 下的 jar 也替换过,任务照样在 init 阶段抛Unknown system variable 'query_cache_size'。这个报错跟 SQL 写法、切分逻辑、并发度都没关系,它来自驱动在连接建立后主动执行的一批会话初始化语句:MySQL 服务端在 8.0.3 之后删掉了query_cache_size这个系统变量,而 5.1.x 的 JDBC 驱动还在按旧版本的做法SET它。datax MySQL8 这个组合之所以长期有人踩,根子就在这一层——DataX 官方的 mysqlreader / mysqlwriter 默认绑定的是 5.1 时代的驱动,插件主体代码对 8.0 几乎无感,真正要改的是驱动版本、连接串参数、插件标识这三层,缺一层任务都起不来。这份插件资源做的就是把这三层一次性拆开重装:读写两侧各一份可独立编译的工程,驱动换成 8.x,连接串补齐 8.0 必需参数,插件名与目录名重新对齐,装进 DataX 后能与老的 mysqlreader 并存,按数据源分流。它适合手里已经有 DataX 调度体系、数据源刚从 5.7 升到 8.0 的团队,也适合第一次给 DataX 写插件、想找个能跑通的模子照着改的人。
2. 驱动与协议层:5.1 的 jar 为什么在 8.0 上必然翻车
DataX 的 mysqlreader 本质是一个薄封装:解析 job.json、拼SELECT、用 JDBC 逐行ResultSet.getXxx()、按 DataX 内部类型送进 Channel。它自己不实现任何网络协议,所有握手、认证、会话初始化都交给驱动。所以“插件能不能连 8.0”这个问题,等价于“libs 里那个 mysql-connector 能不能连 8.0”。把这一层想清楚,后面改 pom、改连接串、看报错才有落点。
2.1 caching_sha2_password:握手阶段的第一个卡点
MySQL 8.0 把默认认证插件从mysql_native_password换成了caching_sha2_password。这个变化对 JDBC 的影响不在服务端,而在客户端有没有能力完成 challenge-response。5.1.x 驱动根本不认识caching_sha2_password,服务端在握手包(HandshakeV10)里把插件名报过来,驱动侧匹配不上,直接抛Unable to load authentication plugin 'caching_sha2_password'。这句话在 DataX 里的表现往往被包了一层,你看到的是Code:[DBUtilErrorCode-10] 连接数据库失败,真正的异常堆栈藏在datax.log里,得往后翻几十行才看得到。
8.x 驱动支持这个插件,但多了一个前置条件:走非加密通道时,它需要向服务端索取 RSA 公钥来加密密码,而这个动作默认是关闭的,于是报Public Key Retrieval is not allowed。两种解法,选一种即可:
- 在 jdbcUrl 上加
allowPublicKeyRetrieval=true,让驱动自动取公钥,适合内网可信链路; - 在服务端把该账号降级为
mysql_native_password(ALTER USER ... IDENTIFIED WITH mysql_native_password BY '...'),适合老客户端一大堆、短期不打算全量升级驱动的环境。
我一般选第一种,因为改 URL 是任务级配置,改认证插件是实例级变更,后者的影响面不可控。但要注意:allowPublicKeyRetrieval=true只在useSSL=false时有意义;如果链路本身开了 TLS,驱动会直接走加密通道传密码,不需要公钥。
2.2 三个高频报错其实同源
Unknown system variable 'query_cache_size'、The server time zone value '中国标准时间' is unrecognized、Establishing SSL connection without server's identity verification is not recommended——这三条常被当成三个独立问题去查,实际上它们是同一件事的三个切面:驱动版本与连接参数不匹配。
第一条是版本问题。8.0.3 之后query_cache_size被移除,5.1.x 驱动在ConnectionImpl初始化时会执行一段会话变量设置,撞上不存在的变量就抛异常。除了query_cache_size,同类还有transaction_isolation与tx_isolation的改名——8.0 用前者,5.1 驱动读后者。这类报错无法靠改 SQL 绕过,只能换驱动。
第二条是时区问题。MySQL 8 的@@system_time_zone在中文环境下会返回CST,而CST在 Java 的时区表里同时对应中国标准时间、美国中部时间、古巴标准时间三个含义,驱动无法判定,直接拒绝连接。加上serverTimezone=Asia/Shanghai显式指定,问题消失。注意别写serverTimezone=GMT%2B8这种十六进制转义形式,某些驱动版本解析会出偏差,写区域名最稳。
第三条是 SSL 提示。8.x 驱动默认尝试建立 TLS,服务端没配证书时驱动会打这条 WARN,但通常还能降级继续;如果服务端强制require_secure_transport=ON,就会直接失败。内网同步任务一律useSSL=false,省掉证书校验开销;跨公网才需要认真配trustCertificateKeyStoreUrl。
2.3 驱动坐标迁移:artifactId 变了,包名也变了
5.1.x 的 Maven 坐标是mysql:mysql-connector-java,8.x 从 8.0.31 起迁到了com.mysql:mysql-connector-j,老坐标虽然还能拉到 8.0.33,但官方已标记为过渡。更关键的是驱动类名:com.mysql.jdbc.Driver在 8.x 里仍然保留但已废弃,推荐com.mysql.cj.jdbc.Driver。DataX 的插件工程里,驱动类名要么来自connection配置的隐式推断,要么在代码里显式Class.forName,改 pom 之后必须同步检查这一处,否则会出现“jar 明明是 8.x,运行时却加载不到类”的怪现象。
| 维度 | 5.1.x | 8.x |
|---|---|---|
| Maven 坐标 | mysql:mysql-connector-java | com.mysql:mysql-connector-j |
| 驱动类 | com.mysql.jdbc.Driver | com.mysql.cj.jdbc.Driver |
| 默认认证插件支持 | 仅 mysql_native_password | 两种都支持 |
| 时区 | 可不配 | 必须配 serverTimezone |
| SSL | 默认关闭 | 默认尝试开启 |
| 批量改写 | 支持 | 支持,且 ServerPreparedStatement 下也生效 |
这张表可以直接当选型依据:只要目标端是 8.0.x,驱动版本必须 ≥ 8.0.16(allowPublicKeyRetrieval在该版本后行为稳定),低于这个版本会碰到公钥缓存相关的偶发认证失败。
3. 插件工程改造:pom、plugin.json、job.json 三处联动
改造 DataX 插件最常见的失败不是代码写错,而是三处标识对不上:pom 里的 artifactId、plugin.json 里的 name、插件目录名、job.json 里的 reader.name。DataX 启动任务时,是拿 job.json 的reader.name去$DATAX_HOME/plugin/reader/下找同名目录,再读该目录的 plugin.json 拿全限定类名,最后用 URLClassLoader 加载libs/*.jar。这条链上任何一环错位,报错信息都会指向一个和真实原因无关的地方。
3.1 复制插件目录并重命名:先定名字,再动代码
不要直接在官方mysqlreader目录上改。原因很实际:线上可能还有连 5.7 的任务在跑,改掉之后老任务全部失效。正确做法是复制一份出来,给一个全新的插件名,比如mysqlreader8,让它和原插件并存。
# 从 DataX 源码树里复制一份 reader 插件工程出来 cp -r datax/mysqlreader ./datax-mysql8-reader cd ./datax-mysql8-reader # 目录改名只是第一步,真正起作用的是 plugin.json 里的 name # 这个名字必须和最终部署目录名、job.json 里的 reader.name 完全一致复制完之后先别急着改代码,把src/main/resources/plugin.json打开,把name从mysqlreader改成mysqlreader8,class字段暂时不动——它是MysqlReader.java里package行加类名的全限定形式,一会儿改完包结构再回来同步。description建议写清楚用途,比如标注“面向 MySQL 8.0.x,驱动 8.0.33”,方便半年后自己回来看。
包名要不要一起改?我建议不改。改包名意味着MysqlReader.java、MysqlReaderErrorCode.java以及内部类里的所有引用都要跟着动,收益只是“看起来干净”,风险是漏改一处编译不过。保持原包名、只改 plugin.json 的 name,是成本最低的隔离方式——JVM 里两个插件类来自不同的 ClassLoader,包名相同也不会冲突。
3.2 pom.xml:换驱动、去旧依赖、控制打包作用域
这一步是整个改造的核心。打开 pom,找到 mysql 驱动那段依赖,整段替换。
<!-- 移除旧的 5.1.x 依赖,注意 groupId 也从 mysql 改成了 com.mysql --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> <!-- 关键:不能是 provided,否则依赖不会被打进 libs,运行时 ClassNotFoundException --> <scope>runtime</scope> </dependency> <!-- 顺手把可能被父 pom 传递进来的老驱动排掉,避免 libs 里同时出现两个版本 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> <scope>provided</scope> </dependency>scope这一项值得多说两句。DataX 插件工程的父 pom 里,很多依赖默认是provided,因为编译期只需要 API。但插件运行在 DataX 自己的 ClassLoader 里,DataX 主程序并不提供 MySQL 驱动,所以驱动的 scope 必须是runtime或compile,让dependency:copy-dependencies能把它拷出来。我见过最隐蔽的一次翻车是:pom 改对了,mvn package也成功,但 libs 目录里只有一个几十 KB 的插件 jar,驱动 jar 根本没生成,任务报ClassNotFoundException: com.mysql.cj.jdbc.Driver,查了半天以为是类名写错。
同时检查一下代码里有没有硬编码的驱动类名。搜索com.mysql.jdbc.Driver,全部替换为com.mysql.cj.jdbc.Driver;如果工程里是通过Connection.getConnection(url, user, pwd)由 DriverManager 自动发现驱动,那这一步可以跳过,但META-INF/services/java.sql.Driver文件必须存在——8.x 的 jar 里自带,不用手动加。
3.3 编译产物落位与三方对齐
改完 pom 就可以出包了。两条命令,第二条容易被忘:
# 打插件自身的 jar,跳过测试省时间 mvn -q clean package -DskipTests # 把运行时依赖全部拷到 target/libs,包括 mysql-connector-j mvn -q dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/libs # 部署到 DataX 插件目录,目录名必须等于 plugin.json 里的 name DATAX_HOME=/opt/datax mkdir -p $DATAX_HOME/plugin/reader/mysqlreader8/libs cp target/datax-mysql8-reader-*.jar target/libs/*.jar $DATAX_HOME/plugin/reader/mysqlreader8/libs/ cp src/main/resources/plugin.json $DATAX_HOME/plugin/reader/mysqlreader8/部署完之后立刻验证插件是否被识别,别等跑任务才发现:
# 这条命令会打印插件的参数模板,能打出来就说明 plugin.json 和 libs 都被正确加载 python3 $DATAX_HOME/bin/datax.py -r mysqlreader8 -w mysqlwriter8参数说明:-r指 reader 插件名,-w指 writer 插件名,脚本会去对应目录读plugin_job_template.json并渲染成一份示例 job.json。如果这里报插件[mysqlreader8]找不到,问题一定在目录名或 plugin.json 的 name 上,与驱动无关,先把这一步跑通再往下走。
最后一处对齐在 job.json:
{ "job": { "content": [ { "reader": { "name": "mysqlreader8", "parameter": { "connection": [ { "jdbcUrl": [ "jdbc:mysql://10.0.0.12:3306/trade?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8" ], "table": ["t_order"] } ] } } } ] } }三个mysqlreader8出现的位置——目录名、plugin.json 的 name、job.json 的 reader.name——必须字面完全一致,大小写敏感。写错一个字母,报错会是“找不到插件”或者更糟:如果在plugin/reader/下还有一个叫mysqlreader的老目录,而你误写成了这个名字,任务会静默地加载老插件,然后抛query_cache_size,让你误以为驱动没换成功。这种“报错指向错误位置”的情况,是排查时最耗时间的一类。
4. Reader 侧落地:splitPk 边界与 MySQL 8 类型映射
插件装好、能连上之后,真正影响同步质量和速度的是 Reader 的参数。DataX 的并发模型是:speed.channel决定同时跑几个 Task,而 Task 数量由 Reader 的切分逻辑决定。mysqlreader 的切分只有一种手段——splitPk。切不动,channel 设成 16 也只有 1 个 Task 在跑,剩下 15 个空转,这是很多人觉得“DataX 慢”的真实原因。
4.1 splitPk 的适用边界:整型主键才切得动
mysqlreader 的切分逻辑是:拿到splitPk指定的列,执行SELECT MIN(pk), MAX(pk) FROM table,然后按 channel 数把区间等分,每个 Task 带上WHERE pk >= ? AND pk < ?。它只认整型,具体是 tinyint / smallint / int / bigint 及其无符号变体。传一个 varchar 主键进去,日志里会出现splitPk not support之类的提示,然后退化成单 Task 全表扫描。
几个实操中的边界:
无符号 bigint 的越界。bigint unsigned的最大值接近 1.8×10^19,超出 JavaLong.MAX_VALUE。切分时MIN/MAX取回来会被驱动转成BigInteger,DataX 侧再做一次类型转换,某些版本会抛NumberFormatException。规避方式是把切分键换成有符号的整型列,或者在 where 里限定范围,比如id < 9223372036854775807,把溢出段单独用单 Task 补一次。
联合主键没有整型列。用雪花 ID、UUID 做主键的表,如果没有任何自增整型列,就只能单 Task 拉。这时候别硬扛,改用“按业务时间分片 + 多次任务”的方式:外层调度器按天或按小时切窗口,每个窗口起一个 DataX 任务,where带上时间范围。这样虽然单任务没并发,但任务之间并行,整体吞吐能接受。
切分键分布不均。MIN/MAX等分假设数据均匀分布。如果主键有大量空洞(比如历史删除过多),会出现某个 Task 分到 80% 的数据,其他 Task 秒完。判断方法是跑一次SELECT COUNT(*) FROM t GROUP BY pk DIV ((MAX-MIN)/N)看一眼分布。不均匀时用where手动限定几个区间,写成多个 content 条目,比让 DataX 自动切更可控。
4.2 MySQL 8 的类型映射表与四个失真点
Reader 读出来的每一列都会被转换成 DataX 内部类型(Long / Double / String / Boolean / Date / Bytes / BigDecimal),再序列化给 Writer。这个转换是隐式的,不报错,但会悄悄丢精度或改语义。
| MySQL 8 类型 | DataX 内部类型 | 坑点 |
|---|---|---|
| tinyint(1) | Boolean | 驱动默认tinyInt1isBit=true,0/1 变 false/true |
| bit(1) | Boolean | bit(n>1) 读成 byte[],落地容易乱码 |
| bigint unsigned | Long / BigDecimal | 超 Long 上限时转换失败 |
| decimal(30,10) | BigDecimal | 精度不丢,但 Writer 端字段长度不够会截断 |
| datetime(6) | Date | 微秒部分丢失,只到秒级 |
| timestamp | Date | 受 serverTimezone 影响,跨时区会偏 |
| json | String | 原样输出 JSON 文本,不解析 |
| enum / set | String | 取的是字符串值,不是索引 |
| geometry / point | 不支持 | 反序列化直接抛异常 |
四个必须处理的失真点:
第一,tinyint(1)。业务里用 0/1 表示状态位很常见,驱动默认把它转成 Boolean,落到目标端就成了true/false,目标表如果是 int 就会报类型不匹配。要么在 URL 上加tinyInt1isBit=false,要么在column里显式写CAST(status AS SIGNED) AS status。
第二,datetime(6)。MySQL 8 默认 datetime 精度是 0,但很多新表建成了datetime(6)。驱动getTimestamp()只保留到毫秒,DataX 的 Date 类型再降到秒,微秒直接归零。对账时如果按时间戳精确比对,会大面积对不上。解决办法是把该列在 column 里写成DATE_FORMAT(created_at,'%Y-%m-%d %H:%i:%s.%f') AS created_at,以字符串形式搬运。
第三,bigint unsigned。同上,超上限就是NumberFormatException,没有优雅降级。要么 cast 成 char 搬运,要么确认业务值域不会越界。
第四,json和geometry。json 列读出来是字符串,长度可能超过目标端 varchar,搬运前确认目标列是 text 或 json 类型。geometry 列 DataX 完全不支持,必须ST_AsText(geo)转 WKT 字符串,或者干脆在 column 里排除。
4.3 增量抽取:where 加 -D 参数替换
全量同步只是第一次,之后都是增量。mysqlreader 支持where条件,但条件是静态字符串,写完就固定了。要让它按调度时间动态变化,靠的是 DataX 的-D参数替换机制。
job.json 里这样写:
{ "reader": { "name": "mysqlreader8", "parameter": { "column": ["id", "order_no", "amount", "status", "updated_at"], "splitPk": "id", "where": "updated_at >= '${start_time}' AND updated_at < '${end_time}'", "connection": [ { "jdbcUrl": ["jdbc:mysql://10.0.0.12:3306/trade?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"], "table": ["t_order"] } ] } } }调度侧这样调:
# -p 后面跟 -D 形式的键值对,DataX 会在解析 job.json 前做文本替换 python3 $DATAX_HOME/bin/datax.py \ -p "-Dstart_time=2024-05-01 00:00:00 -Dend_time=2024-05-02 00:00:00" \ job/t_order_inc.json参数说明:-p的值是一个字符串,里面可以放多个-Dkey=value,用空格分隔;job.json 里用${key}引用。注意两个坑:一是值里含空格时整体要加引号,上面的start_time就带空格,所以-D那段用双引号包住;二是 DataX 的替换是纯文本替换,不做类型转换也不做转义,如果值是用户输入,要自己防注入。
增量同步还有一个隐藏问题:where条件里的时间列如果没有索引,每个 Task 都会全表扫。同步前确认updated_at上有索引,否则单 Task 的耗时会长到让你怀疑是不是卡死了。另外updated_at用ON UPDATE CURRENT_TIMESTAMP自动维护时,同一秒内的多条更新可能被边界条件漏掉或重复,稳妥做法是时间窗口留 1 分钟重叠,靠 Writer 端的replace模式去重。
5. 避坑与排查:五条现象、原因与处置
这一章记录的是改造过程中最容易卡住的五类问题,每一条都按“看到什么、为什么、怎么办”写。它们的共同特征是:报错信息指向的位置,往往不是真正出问题的地方。
5.1 现象:任务报 ClassNotFoundException,类名指向刚改的驱动
现象:任务启动后立刻失败,堆栈里是java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver,但 pom 里明明写了 8.0.33,本地mvn package也没报错。
原因:驱动 jar 没进libs目录。DataX 加载插件时用的是URLClassLoader,扫描路径是plugin/reader/<name>/libs/*.jar,和 Maven 的 classpath 完全无关。scope 写成provided、或者忘了执行dependency:copy-dependencies,都会导致 libs 里只有插件自己的 jar。
解决:ls -l $DATAX_HOME/plugin/reader/mysqlreader8/libs/看有没有mysql-connector-j-8.0.33.jar。没有就回去检查 pom 的 scope,补跑一次dependency:copy-dependencies。还有个容易忽略的点:如果 libs 里同时存在 5.1.x 和 8.x 两个驱动 jar,类加载顺序不确定,可能加载到老的那个,报错却还是query_cache_size。部署前先rm -f libs/mysql-connector-java-*.jar清干净。
5.2 现象:改了 job.json 的 name,加载的还是老插件
现象:job.json 里写的是mysqlreader8,日志里打印的插件路径却是plugin/reader/mysqlreader,跑起来还是老驱动的报错。
原因:DataX 解析reader.name后,按名字拼目录路径。如果plugin/reader/下同时存在mysqlreader和mysqlreader8,而 job.json 里写的是前者(或者拼写只差一个字符),就会静默加载老插件。更隐蔽的情况是:新目录建好了,但 plugin.json 里的name忘了改,仍是mysqlreader,DataX 读到的插件标识和目录名对不上,行为不可预期。
解决:三处对齐检查——目录名、plugin.json 的name、job.json 的reader.name,三者字面完全一致。用python3 datax.py -r mysqlreader8 -w ...先验证插件能被单独识别,再去跑完整任务。日常习惯是:新增插件时在目录里放一个VERSION文件写明驱动版本,出问题时先cat一下,比翻 pom 快。
5.3 现象:Public Key Retrieval is not allowed
现象:连接阶段抛java.sql.SQLException: Public Key Retrieval is not allowed,账号密码确认没写错,用命令行客户端能正常登录。
原因:MySQL 8 默认认证插件是caching_sha2_password,在非 SSL 连接下,客户端需要向服务端索取 RSA 公钥来加密传输密码,而 8.x 驱动出于安全考虑默认禁止这个动作。命令行客户端不受限,所以出现“命令行能连、DataX 连不上”的割裂现象。
解决:jdbcUrl 加allowPublicKeyRetrieval=true&useSSL=false。如果链路上不允许明文,就改用 TLS 并配好信任库。另一种解法是把账号认证方式改成mysql_native_password,但这属于实例级变更,影响所有连这个库的客户端,不推荐为了一个同步任务去动。改完 URL 后记得重启任务,DataX 不会热加载配置。
5.4 现象:Reader 跑满带宽,Writer 慢十倍
现象:同一个任务,reader 阶段每秒几万行,writer 阶段掉到几千行,speed.byte没设限,channel 也加到了 8。
原因:MySQL Writer 默认逐条executeUpdate,没有开批量改写。JDBC 的addBatch在没开rewriteBatchedStatements时,仍然是一条一条发给服务端,batchSize=2048只是攒在客户端内存里,网络往返次数没变。
解决:jdbcUrl 加rewriteBatchedStatements=true,配合batchSize调到 1024~4096。开了这个参数后,驱动会把多条 INSERT 拼成一条 multi-values 语句,往返次数从 N 降到 1。验证方法是开 MySQL 的 general_log 看实际下发的 SQL 长什么样,或者对比开关前后的rows/s。注意这个参数对writeMode=update同样有效,但 update 拼的是UPDATE ... CASE WHEN,语句会很长,batchSize要相应调小到 512 左右,否则容易撞max_allowed_packet。
5.5 现象:跑了几分钟报 Communications link failure
现象:任务前期正常,跑到七八分钟后抛Communications link failure,或者The last packet sent successfully to the server was 0 milliseconds ago。
原因:两种可能。一是服务端wait_timeout到了,连接被回收,而客户端不知道,下次写的时候才失败——这种情况多见于 Reader 慢、Writer 长时间等数据的场景。二是网络中间有设备掐了长连接。区别方法是看报错前的空闲时长是否接近wait_timeout(默认 28800 秒,但很多实例调到了 600 秒)。
解决:URL 上加autoReconnect=true只是治标,驱动重连后 session 状态会丢。更稳的做法是加socketTimeout和connectTimeout,让失败快速暴露而不是挂死;同时在 Writer 侧把batchSize调小,缩短单次写入的间隔。如果确实是wait_timeout太短,让 DBA 给同步账号单独调大,比在客户端做保活可靠。
6. 写入提速与一致性校验:上线前必跑的两件事
插件能跑通只是及格线。真正决定这套东西能不能长期用的,是写入吞吐和事后对账。我一般在上线前固定做两件事:先把 Writer 的 URL 参数调到位,再用分组聚合做一次全量比对,两者都过了才敢挂调度。
6.1 两行 URL 参数把写入拉回正常水位
Writer 侧的 jdbcUrl 和 Reader 侧不完全一样,重点在批量与事务:
{ "writer": { "name": "mysqlwriter8", "parameter": { "writeMode": "replace", "batchSize": 2048, "column": ["id", "order_no", "amount", "status", "updated_at"], "connection": [ { "jdbcUrl": [ "jdbc:mysql://10.0.0.13:3306/trade_bak?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&useServerPrepStmts=false&characterEncoding=utf8" ], "table": ["t_order"] } ] } } }参数逐项说明:rewriteBatchedStatements=true是核心,开启后批量 INSERT 会被拼成 multi-values;useServerPrepStmts=false让它走客户端预编译,改写更彻底,两者组合是社区验证过的提速搭配;batchSize=2048是官方默认值,行宽超过 1KB 时建议降到 512,避免单包超限;writeMode=replace对应REPLACE INTO,靠主键去重,适合幂等重跑,但它会先删后插,如果表上有自增列或触发器,行为可能和预期不符,这类表改用writeMode=update并配好primaryKey。
实测中,不开改写时写入约 3000 行/秒,开了之后同一条链路能到 2 万行/秒以上,提升幅度取决于行宽和索引数量。这个数字不用精确记,知道量级就行,因为它决定了你要不要为了“压测一下”专门搭环境。
6.2 用分组聚合做对账,别用 count(*)
同步完成后的校验,SELECT COUNT(*)只能发现行数差异,发现不了内容错位。我习惯用分组聚合做一次摘要比对:
-- 源端和目标端各跑一次,结果集应完全一致 SELECT DATE(created_at) AS dt, COUNT(*) AS cnt, SUM(CRC32(CONCAT_WS('#', id, IFNULL(order_no,''), IFNULL(amount,''), IFNULL(status,'')))) AS sig FROM t_order WHERE created_at >= '2024-05-01' AND created_at < '2024-05-02' GROUP BY DATE(created_at) ORDER BY dt;CRC32有碰撞概率,但配合分组和行数一起看,足以发现字段错位、精度丢失、编码不一致这几类高频问题。CONCAT_WS用#做分隔符而不是空串,是为了避免a|bc和ab|c拼出同一个值。IFNULL必须有,否则 NULL 参与拼接会让整行变 NULL,签名直接失真。
两边结果不一致时,往下钻一层:把分组粒度从“天”改成“小时”或“主键区间”,缩小范围后再用全字段EXCEPT(MySQL 8.0.31+ 支持)定位到具体行。这个过程我一般控制在两轮以内,再往下就该怀疑是不是同步窗口有重叠导致重复写入了。
血泪经验是:对账 SQL 一定要在同步前先在源端跑一次,确认执行计划和耗时,别等同步完了才发现这条 SQL 在 2 亿行的表上要跑四十分钟。从那以后我每次上线新插件,都强制走一遍“跑十条对账 SQL 看耗时 → 跑全量 → 对账 → 挂调度”这个流程,少一步后面都可能用加班补回来。希望帮到你。
本文还有配套的精品资源,点击获取