Datax-HdfsWriter如何实现支持decimal类型数据写入

本文详细介绍了如何修改DataX的HDFS Writer插件以支持decimal和binary数据类型,通过源码分析和改造,实现了在Hive中使用这些类型的数据同步。

一、问题背景

之前在做Datax数据同步时,发现源端binary、decimal等类型的数据无法写入hive字段。看了一下官网文档,DataX HdfsWriter 插件文档,是1-2年前的,当初看过部分源码其实底层hadoop是支持这些类型写入的,后来随着工作变动也忘了记录下来,借着近期datax群里又有人问起,勾起了回忆,索性改一下源码记录一下。

很重要的一点:我们其实要知道,datax只不过是个集成了异构数据源同步的框架,真正的读取和写入都是数据源底层本身支持功能才能用,所以要想知道某个功能支不支持,首先得去看底层的数据源支不支持。

注:binary类型写入之后读取又会有坑,将另外开启一篇单独介绍Hdfs如何实现支持binary类型数据读写,改动部分代码已提交。

欢迎自取:github地址

分支:feature_hdfs_writer_decimal_binary_support

二. 环境准备

Datax版本:3.0

Hadoop版本:2.7.3

Hive版本:2.3.2

三. Datax 源码

       首先从hdfswriter的startwrite方法入手,根据配置job文件的filetype类型区分写入hdfs的存储格式:

HdfsWriter:

public void startWrite(RecordReceiver lineReceiver) {
            LOG.info("begin do write...");
            LOG.info(String.format("write to file : [%s]", this.fileName));
            if(fileType.equalsIgnoreCase("TEXT")){
                //写TEXT FILE
                hdfsHelper.textFileStartWrite(lineReceiver,this.writerSliceConfig, this.fileName,
                        this.getTaskPluginCollector());
            }else if(fileType.equalsIgnoreCase("ORC")){
                //写ORC FILE
                hdfsHelper.orcFileStartWrite(lineReceiver,this.writerSliceConfig, this.fileName,
                        this.getTaskPluginCollector());
            }

            LOG.info("end do write");
}

       进入hdfsHelper查看具体的写入逻辑:

HdfsHelper:

// TEXT
public void textFileStartWrite(RecordReceiver lineReceiver, Configuration config, String fileName,
                                   TaskPluginCollector taskPluginCollector){
...
            RecordWriter writer = outFormat.getRecordWriter(fileSystem, conf, outputPath.toString(), Reporter.NULL);
            Record record = null;
            while ((record = lineReceiver.getFromReader()) != null) {
                MutablePair<Text, Boolean> transportResult = transportOneRecord(record, fieldDelimiter, columns, taskPluginCollector);
                if (!transportResult.getRight()) {
                    writer.write(NullWritable.get(),transportResult.getLeft());
                }
            }
            writer.close(Reporter.NULL);
...
}

// ORC
public void orcFileStartWrite(RecordReceiver lineReceiver, Configuration config, String fileName,
                                  TaskPluginCollector taskPluginCollector){
...
        List<String> columnNames = getColumnNames(columns);
        // 获取字段类型序列化器,这个方法很关键,后续对于decimal类型字段的改造需要用到
        List<ObjectInspector> columnTypeInspectors = getColumnTypeInspectors(columns);
        StructObjectInspector inspector = (StructObjectInspector)ObjectInspectorFactory
                .getStandardStructObjectInspector(columnNames, columnTypeInspectors);
...
            RecordWriter writer = outFormat.getRecordWriter(fileSystem, conf, fileName, Reporter.NULL);
            Record record = null;
            while ((record = lineReceiver.getFromReader()) != null) {
                MutablePair<List<Object>, Boolean> transportResult =  transportOneRecord(record,columns,taskPluginCollector);
                if (!transportResult.getRight()) {
// orc 格式的需要对应类型序列化器才能写入到hdfs
                    writer.write(NullWritable.get(), orcSerde.serialize(transportResult.getLeft(), inspector));
                }
            }
            writer.close(Reporter.NULL);
...
}

// 将从channel中收到的record字符串按照对应的字段类型进行转换
public static MutablePair<List<Object>, Boolean> transportOneRecord(
            Record record,List<Configuration> columnsConfiguration,
            TaskPluginCollector taskPluginCollector){
...
 for (int i = 0; i < recordLength; i++) {
      column = record.getColumn(i);
      //todo as method
      if (null != column.getRawData()) {
      String rowData = column.getRawData().toString();
      // datax定义的hive支持类型枚举类
      SupportHiveDataType columnType = SupportHiveDataType.valueOf(columnsConfiguration.get(i).getString(Key.TYPE).toUpperCase());
      //根据writer端类型配置做类型转换
      switch (columnType) {
          case TINYINT:
               recordList.add(Byte.valueOf(rowData));
               break;
...
}

      从上述代码中可以得知,text类型文件写入,不需要做特殊的序列化处理,因此对于text类型的文本写入,只要在transportOneRecord中添加缺少的类型转换就能实现对应类型字段的写入,而对于ORC类型的文件写入则需要对应的类型序列化器才能做到。至此我们重点应该放在验证hadoop底层是否真的没有binary以及decimal等类型的序列化器。

      上述代码中,我也标记出了ORC中获取字段序列化器的入口位置[HdfsHelper.getColumnTypeInspectors]方法内部。

HdfsHelper:

// 根据writer配置的字段类型,构建序列化器
public List<ObjectInspector>  getColumnTypeInspectors(List<Configuration> columns){
        List<ObjectInspector>  columnTypeInspectors = Lists.newArrayList();
        for (Configuration eachColumnConf : columns) {
            SupportHiveDataType columnType = SupportHiveDataType.valueOf(eachColumnConf.getString(Key.TYPE).toUpperCase());
            ObjectInspector objectInspector = null;
            switch (columnType) {
                case TINYINT:
                    objectInspector = ObjectInspectorFactory.getReflectionObjectInspector(Byte.class, ObjectInspectorFactory.ObjectInspectorOptions.JAVA);
                    break;
...
}

      看到这里就知道下一步就是需要到ObjectInspectorFactory中去看对应类型的ObjectInspector类是什么,接下来就是到hive的底层源码了。

ObjectInspectorFactory:

public static ObjectInspector getReflectionObjectInspector(Type t, ObjectInspectorFactory.ObjectInspectorOptions options) {
        // 优先从缓存中获取
        ObjectInspector oi = (ObjectInspector)objectInspectorCache.get(t);
        if (oi == null) {
        // 缓存中不存在,获取实际类,并添加到缓存中
            oi = getReflectionObjectInspectorNoCache(t, options);
            objectInspectorCache.put(t, oi);
        }
...
        return oi;
    }



private static ObjectInspector getReflectionObjectInspectorNoCache(Type t, ObjectInspectorFactory.ObjectInspectorOptions options) {
// 开头就验证Map,Array类型的复合字段类型,这就说明了其实hive提供的sdk本身也是支持这些字段类型写入的
        if (t instanceof GenericArrayType) {
            GenericArrayType at = (GenericArrayType)t;
            return getStandardListObjectInspector(getReflectionObjectInspector(at.getGenericComponentType(), options));
        } else {
            if (t instanceof ParameterizedType) {
                ParameterizedType pt = (ParameterizedType)t;
                if (List.class.isAssignableFrom((Class)pt.getRawType()) || Set.class.isAssignableFrom((Class)pt.getRawType())) {
                    return getStandardListObjectInspector(getReflectionObjectInspector(pt.getActualTypeArguments()[0], options));
                }

                if (Map.class.isAssignableFrom((Class)pt.getRawType())) {
                    return getStandardMapObjectInspector(getReflectionObjectInspector(pt.getActualTypeArguments()[0], options), getReflectionObjectInspector(pt.getActualTypeArguments()[1], options));
                }

                t = pt.getRawType();
            }
        if (!(t instanceof Class)) {
                throw new RuntimeException(ObjectInspectorFactory.class.getName() + " internal error:" + t);
            } else {
                Class<?> c = (Class)t;
// 根据传入的不同类去不同的缓存中获取class对象
                if (PrimitiveObjectInspectorUtils.isPrimitiveJavaType(c)) {
                    return PrimitiveObjectInspectorFactory.getPrimitiveJavaObjectInspector(PrimitiveObjectInspectorUtils.getTypeEntryFromPrimitiveJavaType(c).primitiveCategory);
                } else if (PrimitiveObjectInspectorUtils.isPrimitiveJavaClass(c)) {
                    return PrimitiveObjectInspectorFactory.getPrimitiveJavaObjectInspector(PrimitiveObjectInspectorUtils.getTypeEntryFromPrimitiveJavaClass(c).primitiveCategory);
                } else if (PrimitiveObjectInspectorUtils.isPrimitiveWritableClass(c)) {
                    return PrimitiveObjectInspectorFactory.getPrimitiveWritableObjectInspector(PrimitiveObjectInspectorUtils.getTypeEntryFromPrimitiveWritableClass(c).primitiveCategory);
                }
...
}
}

      代码很清晰,直接看对应的缓存class是怎么初始化进去的就可以知道,我们一会需要用什么类型去做代码改造

PrimitiveObjectInspectorUtils:

// 缓存中注册类型
static void registerType(PrimitiveObjectInspectorUtils.PrimitiveTypeEntry t) {
...
        if (t.primitiveJavaType != null) {
            primitiveJavaTypeToTypeEntry.put(t.primitiveJavaType, t);
        }

        if (t.primitiveJavaClass != null) {
            primitiveJavaClassToTypeEntry.put(t.primitiveJavaClass, t);
        }

        if (t.primitiveWritableClass != null) {
            primitiveWritableClassToTypeEntry.put(t.primitiveWritableClass, t);
        }
...
}

// 静态代码块初始化
static {
        binaryTypeEntry = new PrimitiveObjectInspectorUtils.PrimitiveTypeEntry(PrimitiveCategory.BINARY, "binary", byte[].class, byte[].class, BytesWritable.class);
        stringTypeEntry = new PrimitiveObjectInspectorUtils.PrimitiveTypeEntry(PrimitiveCategory.STRING, "string", (Class)null, String.class, Text.class);
        booleanTypeEntry = new PrimitiveObjectInspectorUtils.PrimitiveTypeEntry(PrimitiveCategory.BOOLEAN, "boolean", Boolean.TYPE, Boolean.class, BooleanWritable.class);
        intTypeEntry = new PrimitiveObjectInspectorUtils.PrimitiveTypeEntry(PrimitiveCategory.INT, "int", Integer.TYPE, Integer.class, IntWritable.class);
        longTypeEntry = new PrimitiveObjectInspectorUtils.PrimitiveTypeEntry(PrimitiveCategory.LONG, "bigint", Long.TYPE, Long.class, LongWritable.class);
        floatTypeEntry = new PrimitiveObjectInspectorUtils.PrimitiveTypeEntry(PrimitiveCategory.FLOAT, "float", Float.TYPE, Float.class, FloatWritable.class);
        voidTypeEntry = new PrimitiveObjectInspectorUtils.PrimitiveTypeEntry(PrimitiveCategory.VOID, "void", Void.TYPE, Void.class, NullWritable.class);
        doubleTypeEntry = new PrimitiveObjectInspectorUtils.PrimitiveTypeEntry(PrimitiveCategory.DOUBLE, "double", Double.TYPE, Double.class, DoubleWritable.class);
        byteTypeEntry = new PrimitiveObjectInspectorUtils.PrimitiveTypeEntry(PrimitiveCategory.BYTE, "tinyint", Byte.TYPE, Byte.class, ByteWritable.class);
        shortTypeEntry = new PrimitiveObjectInspectorUtils.PrimitiveTypeEntry(PrimitiveCategory.SHORT, "smallint", Short.TYPE, Short.class, ShortWritable.class);
        dateTypeEntry = new PrimitiveObjectInspectorUtils.PrimitiveTypeEntry(PrimitiveCategory.DATE, "date", (Class)null, Date.class, DateWritable.class);
        timestampTypeEntry = new PrimitiveObjectInspectorUtils.PrimitiveTypeEntry(PrimitiveCategory.TIMESTAMP, "timestamp", (Class)null, Timestamp.class, TimestampWritable.class);
        decimalTypeEntry = new PrimitiveObjectInspectorUtils.PrimitiveTypeEntry(PrimitiveCategory.DECIMAL, "decimal", (Class)null, HiveDecimal.class, HiveDecimalWritable.class);
        varcharTypeEntry = new PrimitiveObjectInspectorUtils.PrimitiveTypeEntry(PrimitiveCategory.VARCHAR, "varchar", (Class)null, HiveVarchar.class, HiveVarcharWritable.class);
        charTypeEntry = new PrimitiveObjectInspectorUtils.PrimitiveTypeEntry(PrimitiveCategory.CHAR, "char", (Class)null, HiveChar.class, HiveCharWritable.class);
        unknownTypeEntry = new PrimitiveObjectInspectorUtils.PrimitiveTypeEntry(PrimitiveCategory.UNKNOWN, "unknown", (Class)null, Object.class, (Class)null);
        registerType(binaryTypeEntry);
        registerType(stringTypeEntry);
        registerType(charTypeEntry);
        registerType(varcharTypeEntry);
        registerType(booleanTypeEntry);
        registerType(intTypeEntry);
        registerType(longTypeEntry);
        registerType(floatTypeEntry);
        registerType(voidTypeEntry);
        registerType(doubleTypeEntry);
        registerType(byteTypeEntry);
        registerType(shortTypeEntry);
        registerType(dateTypeEntry);
        registerType(timestampTypeEntry);
        registerType(decimalTypeEntry);
        registerType(unknownTypeEntry);
    }

       看到这里,就很明白了,hive底层是支持binary,decimal这些类型的字段写入的,所以我们只需要拿到入参的class类。这里用decimal拿来举例子,选择有2个,一个是HiveDecimal.class, HiveDecimalWritable.class,因此回到HdfsHelper中,添加decimal类型,并在枚举类中新增DECIMAL即可

case DECIMAL:
     objectInspector = ObjectInspectorFactory.getReflectionObjectInspector(HiveDecimal.class, ObjectInspectorFactory.ObjectInspectorOptions.JAVA);
break;

但是实际还有个坑,没注意,因为我先测试的text类型的文件写入,在transportOneRecord中用java的decimal去做类型转换操作了

transportOneRecord方法:

case DECIMAL:
recordList.add(new BigDecimal(rowData));
break;

结果运行datax的时候会报错

 java.math.BigDecimal cannot be cast to org.apache.hadoop.hive.common.type.HiveDecimal

后看源码得知,写入时是用对应的ObjectInspector去获取record值,底层最终认的对象只有HiveDecimal类型,所以我们需要将原来的bigdecimal类型改为HiveDecimal即可。

case DECIMAL:
    recordList.add(HiveDecimal.create(new BigDecimal(rowData)));
    break;

ORC写入的源码调用链,有兴趣的可以跟踪看一下,这里不再做描述。

1、HdfsWriter$Task.startWrite->
2、HdfsHelper.orcFileStartWrite->
3、OrcOutputFormat$OrcRecordWriter.write->
4、WriterImpl.addRow->
5、WriterImpl$StructTreeWriter.write->
6、WriterImpl$DecimalTreeWriter.write(支持的类型都有相应的treeWriter)


四、验证结果

将对应的hdfswriter插件进行编译打包,替换到对应的datax/plugins/writer/hdfswriter下

注:打包某个模块指令 mvn -U clean package -pl {模块名} -am -DskipTests


mvn -U clean package -pl hdfswriter -am -DskipTests

mysql中创建对应的表和测试记录:

hive中建orc表:

create table test_decimal(
    id      int,
    money   decimal(10,4)
)
row format delimited
fields terminated by ','
STORED AS ORC
;

最终hive写入结果:

五、代码地址

代码放在我fork的datax上了

github地址

分支:feature_hdfs_writer_decimal_binary_support

 

<think> 首先,用户的问题是关于在DataX-Web中实现MySQL到Hive数据同步的方法。根据系统指令,我需要参考站内引用提供的引用内容。 回顾引用内容: - [^1]: DataX是一个离线数据同步工具,用于各种数据源之间的同步,包括MySQL到Hive。本例中使用DataX实现Hive与MySQL之间的数据传输。 - [^2]: DataX Web是DataX的分布式数据同步工具,提供操作界面,支持创建数据同步任务,包括RDBMS数据源批量创建。提到增量同步,如基于时间或自增主键。 - [^3]: 一个SQL示例,显示插入数据到MySQL表,但从内容看,是hive数据同步到MySQL的示例("hive数据使用datax同步到MySQL")。 - [^4]: 讨论DataX从Hive导出到MySQL时,数据类型映射问题。提到Hive数据类型DataX内部类型的对应关系。 - [^5]: 描述一个DataX专题,关于MySQL导入到Hive的开发样例。包括创建Hive表结构和JSON脚本开发方法。特别提到"方式一 - 基于脚本开发"和"方式二 - 基于DATAX-WEB端配置"。这里明确有MySQL入Hive的示例。 用户的问题聚焦于DataX-Web中实现MySQL到Hive数据同步的方法。引用[5]直接提供了这个方法:它描述了两种方式: 1. 基于脚本开发:创建JSON配置文件。 2. 基于DATAX-WEB端配置:通过Web界面配置。 具体在引用[5]中提到:"第二章 Datax专题之Mysql入Hive开发样例(方式一 - 基于脚本开发 & 方式二 - 基于DATAX-WEB端配置)",并给出了创建目录和JSON脚本的步骤。 所以,我的回答应该基于引用[5],因为它最相关。我需要解释如何在DataX-Web中配置MySQL到Hive的同步。 回答结构: - 系统指令要求回答结构清晰,帮助用户逐步解决问题。 - 使用中文回答。 - 所有行内数学表达式用$...$,但我这里没有数学表达式,所以忽略。 - 独立公式用$$...$$,但这里不需要。 - 末尾自然地添加引用标识。 - 生成相关问题。 步骤: 1. **介绍方法**:解释DataX-Web提供两种配置方式:基于脚本和基于Web界面。 2. **详细步骤**:    - **基于脚本开发**:      - 创建JSON配置文件定义数据源和目标。      - 引用[5]给出了示例:如创建Hive表结构和JSON脚本。    - **基于Web界面配置**:      -DataX-Web中创建任务,选择MySQL作为源,Hive作为目标。      - 通过页面选择和配置参数。 3. **增量同步**:引用[2]提到支持增量同步,如基于时间或自增主键。 4. **数据类型映射**:引用[4]提醒数据类型兼容性问题,需要注意。 5. **添加引用**:在关键部分标注引用。 6. **生成相关问题**:在结尾添加相关问题段落。 完整回答: - 开头:简要回答用户问题。 - 主体:分步骤解释方法。 - 结尾:引用标识和相关问题。 相关问题生成:§§相关问题§§ 基于主题,可能的问题如: 1. 如何在DataX-Web中配置增量同步? 2. DataX处理数据类型映射的最佳实践是什么? 3. 优化DataX同步性能的技巧有哪些? 确保回答真实可靠:基于引用内容,不添加虚构信息。 </think> 基于您的查询,我将详细解释在DataX-Web中实现MySQL到Hive数据同步的方法。DataX-Web是DataX的可视化扩展,简化了任务配置过程,支持通过Web界面创建和管理同步任务。以下方法参考了站内引用提供的技术细节(尤其是引用[5]),确保步骤清晰、可靠。核心方法包括两种方式:**基于脚本开发**和**基于Web界面配置**,我将逐步说明操作流程。 ### 1. **基于脚本开发方式** 这是一种传统方法,需要手动编写JSON配置文件定义数据源和目标。优点是灵活性强,适用于复杂场景;缺点是需要熟悉DataX语法。步骤基于引用[5]中的示例: - **步骤1: 创建必要的目录和文件** - 在服务器上创建存放配置文件的核心目录: ```bash mkdir db_conf # 存放数据库连接信息 mkdir json_conf # 存放DataX任务JSON配置文件 mkdir sh_conf # 存放执行脚本 ``` - **步骤2: 编写JSON配置文件** - 在`json_conf`目录下创建JSON文件(如`mysql_to_hive.json`),配置任务参数: ```json { "job": { "content": [ { "reader": { "name": "mysqlreader", "parameter": { "username": "your_mysql_user", "password": "your_mysql_password", "column": ["id", "bank_name", "etl_time"], // 选择MySQL源表字段 "connection": [ { "table": ["source_table"], // MySQL源表名 "jdbcUrl": ["jdbc:mysql://mysql_host:3306/db_name"] } ] } }, "writer": { "name": "hdfswriter", // DataX内部使用hdfswriter写入Hive "parameter": { "defaultFS": "hdfs://hadoop102:8020", // HDFS地址 "fileType": "text", // 文件类型,通常为text或orc "path": "/user/hive/warehouse/ods.db/target_table", // Hive表HDFS路径 "fileName": "data_file", "writeMode": "append", // 写入模式(append/truncate等) "fieldDelimiter": ",", // 字段分隔符 "column": [ {"name": "id", "type": "INT"}, // 字段映射,参考Hive表结构 {"name": "bank_name", "type": "STRING"}, {"name": "etl_time", "type": "STRING"} ] } } } ], "setting": { "speed": { "channel": 3 // 并发通道数,优化性能 } } } } ``` - **关键点**: - `reader`部分配置MySQL连接细节;`writer`部分指向Hive表的HDFS路径(因为Hive数据存储在HDFS)。 - 字段映射需严格对齐MySQL和Hive的数据类型(例如,MySQL的VARCHAR映射到Hive的STRING),以避免脏数据问题(参考引用[4])。 - Hive表结构需预先创建,如引用[5]中的示例:`CREATE TABLE ods_T_YYBZB_TGH_BANKINFO_di(...)`。 - **步骤3: 在DataX-Web中导入和执行脚本** - 登录DataX-Web界面,转到“任务管理”部分。 - 上传JSON文件,并设置调度参数(如定时执行)。 - 运行任务,DataX-Web会调用DataX引擎执行同步。 ### 2. **基于Web界面配置方式** 这是推荐方法,适合非技术用户。DataX-Web提供图形化界面,无需手动编码。步骤简洁高效(引用[2]和[5]): - **步骤1: 配置数据源** - 登录DataX-Web,进入“数据源管理”。 - 添加MySQL数据源: - 类型:MySQL - JDBC URL、用户名、密码等(类似`jdbc:mysql://mysql_host:3306/db_name`)。 - 添加Hive数据源: - 类型HDFS(间接指向Hive),配置HDFS地址(如`hdfs://hadoop102:8020`)。 - 注意:Hive本身不是直接数据源,需通过HDFS路径关联(引用[5])。 - **步骤2: 创建同步任务** - 进入“任务管理” > “新建任务”。 - 选择源:MySQL数据源,指定表名(如`source_table`)和需同步的字段。 - 选择目标:Hive/HDFS数据源,指定HDFS路径(如`/user/hive/warehouse/ods.db/target_table`)。 - 配置映射: - 自动映射字段,或手动调整数据类型(例如,确保MySQL的INT映射到Hive的INT)。 - 设置写入模式(append覆盖增量数据,truncate全量覆盖)。 - 高级选项: - **增量同步**:如果需增量更新,启用“增量配置”,选择基于时间戳或自增主键(如`id`字段),参考引用[2]的功能描述。 - **步骤3: 执行和监控** - 保存任务后,点击“运行”按钮。 -DataX-Web的“任务日志”中实时查看进度和错误信息(如数据类型映射错误)。 - 支持终止任务或设置定时调度(集成xxl-job,引用[2])。 ### 关键注意事项 - **数据类型映射**:MySQL和Hive数据类型不完全兼容(如MySQL的DATETIME需转换为Hive的STRING或TIMESTAMP)。DataX内部类型需对齐,避免脏数据(例如,使用`String`类型处理Hive的复杂类型如MAP或STRUCT,参考引用[4])[^4]。 - **性能优化**:增加`channel`参数提高并发(基于脚本方式),或在Web界面调整“速度设置”。 - **增量处理**:对于大数据量,优先使用增量同步(基于时间或自增主键),减少全量同步开销[^2]。 - **事前准备**:确保Hive表已创建,结构与MySQL源表匹配(引用[5]中的CREATE TABLE语句示例)[^5]。 通过上述方法,您可以在DataX-Web中高效实现MySQL到Hive的数据同步。Web界面方式更易上手,而脚本方式适合定制化需求。实际部署时,建议先测试小规模数据验证配置正确性。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值