简介:这份资源是经过源码改造的DataX数据同步引擎,专门解决PostgreSQL中geometry地理空间类型无法通过原生DataX正常同步的问题,面向需要处理空间数据ETL的数据库开发与数据集成工程师。包内共113个文件,以97个jar依赖为核心,辅以10个json配置、3个Python脚本、1个xml、1个md说明及1个properties文件,压缩包约56.67MB,其中plugin目录下的reader与writer仅保留postgresql和rdbms模块,结构精简、开箱即用。使用时只需将下载的DataX压缩包替换本地DataX即可,若需接入其他数据库,把对应模块复制进plugin目录便能扩展。目前已有490人学习下载,适合希望快速打通PostGIS空间字段同步链路、减少自行改造引擎成本的开发者参考,可省去从零调试源码的时间,直接获得可运行的改造版本与依赖清单。
1. DataX 改造引擎支持 geometry 类型同步:从丢字段到空间数据完整落库
做数仓的同学大概率遇到过这种场景:MySQL 里存着POINT、POLYGON、MULTIPOLYGON这类 geometry 字段,用 DataX 抽到 Hive 或者另一套 MySQL 时,任务不报错,但目标表里那一列全是null,或者干脆被截断成一段乱码。我第一次碰到这个问题时排查了大半天,最后发现是 DataX 内置的 MySQL Reader 和 Writer 对 geometry 类型没有做专门处理,走的是默认的字符串转换逻辑,二进制 WKB 数据在中间环节被静默丢弃。这篇笔记就围绕「DataX 改造引擎支持 geometry 类型同步」这件事,把原理、改造点、参数配置和踩坑记录讲清楚。适合正在做空间数据同步、GIS 平台数据迁移、或者单纯被 geometry 字段坑过的后端和数据工程师。读完你能自己动手改 Reader 和 Writer 插件,让 geometry 字段完整地从源库落到目标库。
2. 为什么 DataX 默认同步不了 geometry:类型映射与二进制链路拆解
2.1 DataX 的类型转换机制到底在哪一层丢数据
DataX 的核心设计是 Reader 和 Writer 解耦,中间通过Record和Column传递数据。Reader 负责从源库读取数据并转换成 DataX 内部类型,Writer 负责把内部类型再转成目标库的类型。问题就出在这个「内部类型」上。
DataX 内部定义的类型非常有限,主要是Long、Double、String、Boolean、Date、Bytes这几种。MySQL Reader 在读取时,会通过ResultSetMetaData.getColumnType()拿到列的 JDBC 类型,然后走一个switch分支映射到内部类型。geometry 字段在 JDBC 里对应的类型是BINARY或VARBINARY,但 MySQL Connector/J 对 geometry 列返回的getColumnType()有时是LONGVARBINARY,有时是BINARY,取决于驱动版本和useInformationSchema等参数。
关键问题在于:MySQL Reader 默认的getColumnType映射表里,BINARY/VARBINARY这类类型如果没有显式配置,会被归到Bytes或者直接走String转换。而 geometry 的二进制是 WKB 格式,里面包含字节0x00,一旦被当成字符串处理,遇到0x00就截断了。这就是为什么你看到的结果要么是null,要么是一小段乱码。
另一个丢数据的地方在 Writer 端。MySQL Writer 在写入时,会根据目标列的columnType决定用setString还是setBytes。如果目标列是 geometry 类型,但 Writer 拿到的是String类型的内部值,它可能会调用setString,而 MySQL 对 geometry 列的setString行为是不确定的,很多版本直接报错或者写入空值。
2.2 geometry 在 MySQL 里的存储格式与 WKB 字节序
要改造,先得搞清楚 geometry 在 MySQL 里到底存的是什么。MySQL 的 geometry 类型底层存储是 WKB(Well-Known Binary)格式,外加 4 个字节的 SRID(空间参考系 ID)。具体结构是:
| 部分 | 长度 | 说明 |
|---|---|---|
| SRID | 4 字节 | 小端序整数,标识空间参考系 |
| WKB | 变长 | OGC 标准定义的几何二进制 |
WKB 本身第一个字节是字节序标记:0x01表示小端序,0x00表示大端序。第二个到第五个字节是几何类型(1=Point, 2=LineString, 3=Polygon, 4=MultiPoint, 5=MultiLineString, 6=MultiPolygon, 7=GeometryCollection)。后面跟着坐标数据。
MySQL 在SELECT一个 geometry 列时,默认返回的是不带 SRID 的 WKB(除非你用ST_AsWKB或者直接查内部格式)。但如果你用SELECT geom_column FROM table,JDBC 驱动返回的byte[]实际上是 MySQL 内部格式,也就是带 4 字节 SRID 的。这一点非常关键,因为如果你在 Reader 里直接把这个byte[]透传给 Writer,Writer 再用setBytes写回去,MySQL 是能识别的。但如果你中间做了任何字符串转换,SRID 那 4 个字节可能就被吃掉了。
2.3 改造点定位:Reader 端和 Writer 端各要动什么
改造的核心思路是:让 Reader 把 geometry 列读成Bytes类型,并且保留完整的字节数组;让 Writer 在遇到目标列为 geometry 时,用setBytes写入。
Reader 端需要改的地方在com.alibaba.datax.plugin.reader.mysqlreader.MysqlReader$Task类里,具体是getColumnType方法或者transportOneRecord方法。常见做法是增加一个判断:如果columnType是BINARY、VARBINARY、LONGVARBINARY或者GEOMETRY,就强制走Bytes分支,并且用resultSet.getBytes(columnIndex)而不是getString。
Writer 端需要改的地方在com.alibaba.datax.plugin.writer.mysqlwriter.MysqlWriter$Task里,具体是fillPreparedStatement方法。当内部Column的类型是Bytes且目标列类型是 geometry 时,调用preparedStatement.setBytes(index, (byte[]) column.getRawData())。
还有一个容易被忽略的点:DataX 的Column类在setRawData时,如果传入的是byte[],它会正确存储。但如果你在 Reader 里用了String类型,Column会调用toString(),那就全毁了。所以改造的第一步是确保 Reader 输出的是Bytes类型。
提示:不同版本的 DataX 源码包结构略有差异,但核心类名和包路径基本一致。改造前先确认你用的是哪个版本,建议基于官方 master 分支或者稳定 release 版本改,不要在生产环境直接改编译好的 jar。
3. 动手改 Reader:让 geometry 列以 Bytes 类型完整读出
3.1 定位 MysqlReader 的类型映射代码
先把 DataX 源码拉下来,找到mysqlreader插件模块。路径通常是datax/plugin/reader/mysqlreader/src/main/java/com/alibaba/datax/plugin/reader/mysqlreader/MysqlReader.java。里面有一个内部类Task,继承自Reader.Task。
在Task类里,找到startRead或者transportOneRecord方法。通常长这样:
private void transportOneRecord(Record record, ResultSet resultSet, ...) { for (int i = 0; i < columnCount; i++) { String columnType = resultSetMetaData.getColumnTypeName(i + 1); // 默认走字符串 record.setColumn(i, new StringColumn(resultSet.getString(i + 1))); } }这段代码就是罪魁祸首。resultSet.getString对 geometry 列会返回乱码或者 null。我们要改成根据列类型判断。
3.2 增加 geometry 类型判断与 Bytes 分支
改造后的代码逻辑如下:
private void transportOneRecord(Record record, ResultSet resultSet, ResultSetMetaData metaData, int columnCount) throws SQLException { for (int i = 0; i < columnCount; i++) { int columnIndex = i + 1; String columnTypeName = metaData.getColumnTypeName(columnIndex).toUpperCase(); int columnType = metaData.getColumnType(columnIndex); // 判断是否为 geometry 或二进制类型 if ("GEOMETRY".equals(columnTypeName) || "POINT".equals(columnTypeName) || "LINESTRING".equals(columnTypeName) || "POLYGON".equals(columnTypeName) || "MULTIPOINT".equals(columnTypeName) || "MULTILINESTRING".equals(columnTypeName) || "MULTIPOLYGON".equals(columnTypeName) || "GEOMETRYCOLLECTION".equals(columnTypeName) || columnType == Types.BINARY || columnType == Types.VARBINARY || columnType == Types.LONGVARBINARY) { byte[] bytes = resultSet.getBytes(columnIndex); if (bytes == null) { record.setColumn(i, new BytesColumn(null)); } else { record.setColumn(i, new BytesColumn(bytes)); } } else { // 原有逻辑 String value = resultSet.getString(columnIndex); record.setColumn(i, new StringColumn(value)); } } }这段代码的核心改动是:先通过getColumnTypeName拿到数据库层面的类型名,再结合getColumnType的 JDBC 类型做双重判断。为什么两个都要?因为 MySQL 驱动对 geometry 列的getColumnType返回值不稳定,有的版本返回BINARY,有的返回VARBINARY,但getColumnTypeName通常能稳定返回GEOMETRY或者具体的子类型名。两个条件用||连接,覆盖面更广。
BytesColumn是 DataX 内部类型,构造时传入byte[]。注意这里不要做任何编码转换,直接透传原始字节。resultSet.getBytes对 geometry 列返回的就是 MySQL 内部格式的字节数组,包含 SRID 和 WKB。
3.3 编译打包与替换插件
改完代码后,在mysqlreader模块目录下执行 Maven 打包:
cd datax/plugin/reader/mysqlreader mvn clean package -DskipTests打包完成后,在target目录下会生成mysqlreader-xxx.jar。把这个 jar 替换到 DataX 安装目录的plugin/reader/mysqlreader/下,注意保留原来的plugin.json和libs目录结构。
如果你不想动源码,还有一个取巧的办法:在 DataX 的 job.json 里,把 geometry 列配置成"type": "bytes"。但实测这个办法对 MySQL Reader 不生效,因为 Reader 端根本不认这个配置,它只认自己的类型映射逻辑。所以还是得改代码。
注意:替换 jar 之前先备份原文件。如果 DataX 是集群部署,每个节点都要替换。替换后建议先用一个只有几行数据的小表做验证,不要直接上生产大表。
4. 动手改 Writer:让 Bytes 类型正确写入 geometry 列
4.1 定位 MysqlWriter 的 PreparedStatement 填充逻辑
Writer 端的代码在datax/plugin/writer/mysqlwriter/src/main/java/com/alibaba/datax/plugin/writer/mysqlwriter/MysqlWriter.java。找到内部类Task里的fillPreparedStatement方法,或者叫doOneInsert。
默认逻辑大概是:
private void fillPreparedStatement(PreparedStatement ps, Record record, List<Column> columns) throws SQLException { for (int i = 0; i < columns.size(); i++) { Column column = columns.get(i); switch (column.getType()) { case STRING: ps.setString(i + 1, column.asString()); break; case LONG: ps.setLong(i + 1, column.asLong()); break; // ... 其他类型 default: ps.setObject(i + 1, column.getRawData()); } } }问题在于default分支。如果column.getType()是BYTES,它可能会走setObject,而 MySQL 驱动对setObject传入byte[]到 geometry 列的行为是不确定的。我们需要显式处理BYTES类型。
4.2 增加 BYTES 类型到 setBytes 的映射
改造后的代码:
private void fillPreparedStatement(PreparedStatement ps, Record record, List<Column> columns) throws SQLException { for (int i = 0; i < columns.size(); i++) { Column column = columns.get(i); switch (column.getType()) { case STRING: ps.setString(i + 1, column.asString()); break; case LONG: ps.setLong(i + 1, column.asLong()); break; case DOUBLE: ps.setDouble(i + 1, column.asDouble()); break; case BOOLEAN: ps.setBoolean(i + 1, column.asBoolean()); break; case DATE: ps.setTimestamp(i + 1, new Timestamp(column.asDate().getTime())); break; case BYTES: byte[] rawBytes = (byte[]) column.getRawData(); if (rawBytes == null) { ps.setNull(i + 1, Types.BINARY); } else { ps.setBytes(i + 1, rawBytes); } break; default: ps.setObject(i + 1, column.getRawData()); } } }关键点是case BYTES分支。column.getRawData()返回的是Object,实际类型是byte[],强转后调用ps.setBytes。MySQL 驱动在收到setBytes时,如果目标列是 geometry 类型,它会直接把字节数组写入,不做额外转换。前提是字节数组的格式必须是 MySQL 内部格式(带 SRID 的 WKB),而 Reader 端getBytes拿到的正好就是这个格式,所以能无缝对接。
如果目标列是POINT而不是GEOMETRY,setBytes同样有效,因为 MySQL 内部对这两种类型的存储格式是一样的,只是约束不同。
4.3 目标表 DDL 与列类型对齐检查
Writer 改完之后,还要确认目标表的 DDL。如果源表是GEOMETRY,目标表也必须是GEOMETRY或者对应的子类型。如果目标表建成了BLOB或者VARBINARY,虽然setBytes也能写进去,但数据就失去了空间语义,后续没法用ST_AsText等函数查询。
常见做法是目标表 DDL 和源表保持一致:
CREATE TABLE `target_table` ( `id` BIGINT NOT NULL, `geom` GEOMETRY DEFAULT NULL, `name` VARCHAR(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;如果目标表是 Hive,那又是另一回事。Hive 没有原生 geometry 类型,通常用STRING存 WKT 或者BINARY存 WKB。这种情况下 Writer 端需要额外做一次ST_AsWKB或者ST_AsText转换,但那就不是 DataX 插件层面能解决的了,得在 SQL 里用SELECT ST_AsText(geom)提前转好。这篇笔记聚焦的是 MySQL 到 MySQL 的 geometry 同步,跨异构存储的转换后面再说。
提示:建目标表时,字符集建议用
utf8mb4,虽然 geometry 列本身不受字符集影响,但同一张表里的其他字符串列可能会因为字符集不一致导致写入失败。
5. 避坑与排查:geometry 同步改造中最容易翻车的 5 个点
5.1 现象:任务不报错但目标表 geometry 列全是 null
原因:Reader 端没有改,仍然走getString,而 geometry 的 WKB 字节里包含0x00,被字符串截断后变成空值,Writer 收到空值就写 null。
解决:确认 Reader 端transportOneRecord方法里已经增加了 geometry 判断分支,并且用的是resultSet.getBytes。改完后重新编译打包,替换 jar 后重启 DataX 进程。验证方法是先跑一条SELECT geom FROM source_table LIMIT 1,在 Reader 里打日志输出字节数组长度,确认不是 0。
5.2 现象:Writer 报错 "Cannot get geometry object from data you send to the GEOMETRY field"
原因:Writer 端虽然走了setBytes,但传入的字节数组不是 MySQL 内部格式。常见于 Reader 端用了ST_AsWKB(geom)查询,返回的是纯 WKB 不带 SRID,而 MySQL 的 geometry 列要求带 SRID 前缀。
解决:要么在 Reader 端直接SELECT geom拿内部格式,要么在 Writer 端写入前手动补上 4 字节 SRID。补 SRID 的代码逻辑是:取原始 WKB 的前 5 个字节判断字节序和类型,然后在前面插入 4 字节的 SRID(通常用 0 表示无参考系,或者 4326 表示 WGS84)。但更简单的做法是直接用SELECT geom,让 MySQL 驱动返回内部格式。
5.3 现象:同步到一半任务失败,日志显示 "Data truncation: Cannot create a geometry object from data"
原因:源表里有的 geometry 列值是 null,或者包含非法几何数据。Reader 端getBytes返回 null 时,如果 Writer 端没有处理 null 分支,setBytes会报错。
解决:在 Writer 的case BYTES分支里增加 null 判断,用ps.setNull(i + 1, Types.BINARY)代替ps.setBytes。同时检查源表数据质量,用SELECT COUNT(*) FROM source_table WHERE geom IS NULL OR ST_IsValid(geom) = 0排查非法数据。
5.4 现象:小表同步正常,大表同步时内存溢出
原因:geometry 字段的 WKB 数据可能很大,尤其是MULTIPOLYGON类型,一个字段可能几 MB。DataX 默认的channel和batchSize配置下,大量 geometry 数据堆积在内存里导致 OOM。
解决:调小batchSize,在 job.json 的setting.speed里把byte限制调低,或者增加channel数量分散压力。另外可以在 Reader 端增加流式读取逻辑,不要一次性把所有 Record 都缓存在内存里。实测把batchSize从 2048 降到 512,同时channel从 4 增加到 8,能有效缓解大 geometry 字段的 OOM 问题。
5.5 现象:改造后 MySQL 到 MySQL 正常,但 MySQL 到 Oracle 或 PostgreSQL 失败
原因:不同数据库对 geometry 的二进制格式要求不同。Oracle 的SDO_GEOMETRY是自定义对象类型,PostgreSQL 的 PostGIS 用的是 EWKB 格式,和 MySQL 的内部格式不兼容。
解决:跨异构数据库同步 geometry 时,不要走二进制透传,而是在 Reader 端用ST_AsText转成 WKT 字符串,Writer 端再用目标库的函数转回 geometry。比如 MySQL 到 PostgreSQL,Reader SQL 写成SELECT ST_AsText(geom) FROM source_table,Writer 端配置 PostgreSQL 的ST_GeomFromText函数。这种方式会损失一些精度和性能,但兼容性最好。
注意:跨库同步时,SRID 也要对齐。MySQL 里 SRID 4326 和 PostgreSQL 里 SRID 4326 虽然语义一样,但二进制格式不同,必须走 WKT 中转。
6. 进阶技巧:用 DataX 做增量 geometry 同步与数据校验
改造完 Reader 和 Writer 只是第一步,实际生产环境里还要解决增量同步和数据校验的问题。我一般会这么干。
增量同步方面,geometry 字段本身没有时间戳,所以增量依赖业务表的主键或者更新时间字段。在 job.json 的 Reader SQL 里加上WHERE update_time > '${last_sync_time}',然后用 DataX 的调度工具传入变量。注意 geometry 字段在增量同步时同样要走改造后的 Bytes 分支,不要因为加了 WHERE 条件就改回getString。
数据校验方面,同步完成后不能只看任务成功日志,得实际比对源表和目标表的 geometry 数据。我常用的校验 SQL 是:
-- 在源库执行 SELECT id, ST_AsText(geom) AS wkt, ST_SRID(geom) AS srid FROM source_table WHERE update_time > '2024-01-01 00:00:00'; -- 在目标库执行同样的 SQL,然后比对结果如果 WKT 和 SRID 都一致,说明同步没问题。如果 WKT 一致但 SRID 不一致,说明 Writer 端写入时 SRID 丢了,需要检查 Reader 端是否用了ST_AsWKB而不是直接SELECT geom。
还有一个进阶玩法是用 DataX 的transformer功能,在 Reader 和 Writer 之间插入自定义转换逻辑。比如源表 SRID 是 4326,目标表要求 3857,可以在 transformer 里调用ST_Transform。但 DataX 的 transformer 对二进制类型支持有限,实测直接传byte[]进去会报类型转换错误,所以这个方案我试了一次就放弃了,还是老老实实在 Reader SQL 里用ST_Transform(geom, 3857)提前转好。
最后说一个我踩过的坑:DataX 的 job.json 里,column配置如果写了"type": "string",即使 Reader 端改成了 Bytes,Writer 端也会因为配置强制转成 String。所以 job.json 里 geometry 列的 type 要么不写,要么写"type": "bytes"。这个细节坑了我一个下午,血泪经验。
希望帮到你。
本文还有配套的精品资源,点击获取