Datax-Mysql同步至Hive时时区问题导致日期减少一天

一、问题背景

Datax从Mysql数据库到Hive数据同步任务时,发现同步时,date类型字段同步到Hive里中string类型字段时,部分日期会减少一天。

二、集群环境

Datax版本:3.0

三、现象描述

mysql数据库时间 Hive数据库中时间
1986-07-17 1986-07-16
1987-06-26 1987-06-25
1987-12-08 1987-12-08
1988-05-18 1988-05-17
1989-08-08 1989-08-07
1997-08-08 1997-08-08

以上数据是测试时,发现的情况,可以看到对于某些特定日期数据来说,日期往前推移了一天。而且hive表中接收的字段必须是string类型的,对于hive也是date类型的数据不会有这种问题,对于这种现象令人十分迷惑,只能先从代码上找找原因所在,但可以确定大概率的问题应该出在类型转换上。

四、Datax代码

Reader代码Date类型转换:

CommonRdbmsReader:

protected Record buildRecord(RecordSender recordSender,ResultSet rs, ResultSetMetaData metaData, int columnNumber, String mandatoryEncoding,
        		TaskPluginCollector taskPluginCollector) {
        	Record record = recordSender.createRecord();

            try {
                for (int i = 1; i <= columnNumber; i++) {
                    switch (metaData.getColumnType(i)) {

                    ...
    
                    // for mysql bug, see http://bugs.mysql.com/bug.php?id=35115
                    case Types.DATE:
                        if (metaData.getColumnTypeName(i).equalsIgnoreCase("year")) {
                            record.addColumn(new LongColumn(rs.getInt(i)));
                        } else {
                            record.addColumn(new DateColumn(rs.getDate(i)));
                        }
                        break;

                    ...

                    }
                }
            } catch (Exception e) {
                if (IS_DEBUG) {
                    LOG.debug("read data " + record.toString()
                            + " occur exception:", e);
                }
                //TODO 这里识别为脏数据靠谱吗?
                taskPluginCollector.collectDirtyRecord(record, e);
                if (e instanceof DataXException) {
                    throw (DataXException) e;
                }
            }
            return record;
        }

Writer端String转换代码:

HdfsHelper:


public static MutablePair, Boolean> transportOneRecord(
            Record record,List columnsConfiguration,
            TaskPluginCollector taskPluginCollector){

        MutablePair, Boolean> transportResult = new MutablePair, Boolean>();
        transportResult.setRight(false);
        int recordLength = record.getColumnNumber();
        if (0 == recordLength) {
            transportResult.setRight(true);
            return transportResult;
        }

        int maxOutputColumnIndex = getMaxColumnIndex(columnsConfiguration);
        Object[] records = new Object[maxOutputColumnIndex + 1];
        Arrays.fill(records, null);

        int recordIndex = 0;
        for (Configuration columnCof : columnsConfiguration) {
            Column recordColumn = record.getColumn(recordIndex);
            if (null != recordColumn.getRawData()) {
                String rowData = recordColumn.getRawData().toString();
                SupportHiveDataType columnType = SupportHiveDataType.valueOf(
                        columnCof.getString(Key.TYPE).toUpperCase());
                //根据writer端类型配置做类型转换
                try {
                    switch (columnType) {
                        ...
                        case STRING:
                        case VARCHAR:
                        case CHAR:
                            records[columnCof.getInt(Key.INDEX, recordIndex)] = recordColumn.asString();
                            break;
                        case DATE:
                            records[columnCof.getInt(Key.INDEX, recordIndex)] = new java.sql.Date(recordColumn.asDate().getTime());
                            break;
                        case TIMESTAMP:
                            records[columnCof.getInt(Key.INDEX, recordIndex)] = new java.sql.Timestamp(recordColumn.asDate().getTime());
                            break;
                        default:
                            throw DataXException
                                    .asDataXException(
                                            HdfsWriterErrorCode.ILLEGAL_VALUE,
                                            String.format(
                                                    "您的配置文件中的列配置信息有误. 因为DataX 不支持数据库写入这种字段类型. 字段名:[%s], 字段类型:[%d]. 请修改表中该字段的类型或者不同步该字段.",
                                                    columnCof.getString(Key.NAME),
                                                    columnCof.getString(Key.TYPE)));
                    }
                } catch (Exception e) {
                    // warn: 此处认为脏数据
                    String message = String.format(
                            "字段类型转换错误:你目标字段为[%s]类型,实际字段值为[%s].",
                            columnCof.getString(Key.TYPE), recordColumn.getRawData().toString());
                    taskPluginCollector.collectDirtyRecord(record, message);
                    transportResult.setRight(true);
                    break;
                }
            }else {
                // warn: it's all ok if nullFormat is null
                records[columnCof.getInt(Key.INDEX, recordIndex)] = null;
            }
            recordIndex++;
        }
        transportResult.setLeft(Arrays.asList(records));
        return transportResult;
    }

可以看出来,主要都是通过DateColumn这个类,进行源端date类型数据封装,在通过asString读取数据。

public class DateColumn extends Column {

	private DateType subType = DateType.DATETIME;

	public static enum DateType {
		DATE, TIME, DATETIME
	}

	/**
	 * 构建值为stamp(Unix时间戳)的DateColumn,使用Date子类型为DATETIME
	 * 实际存储有date改为long的ms,节省存储
	 * */
	public DateColumn(final Long stamp) {
		super(stamp, Column.Type.DATE, (null == stamp ? 0 : 8));
	}

	/**
	 * 构建值为date(java.sql.Date)的DateColumn,使用Date子类型为DATE,只有日期,没有时间
	 * */
	public DateColumn(final java.sql.Date date) {
		this(date == null ? null : date.getTime());
		this.setSubType(DateType.DATE);
	}

	@Override
	public String asString() {
		try {
			return ColumnCast.date2String(this);
		} catch (Exception e) {
			throw DataXException.asDataXException(
					CommonErrorCode.CONVERT_NOT_SUPPORT,
					String.format("Date[%s]类型不能转为String .", this.toString()));
		}
	}
}

看一下Date to String时做的转换类

public final class ColumnCast {
	...
	public static String date2String(final DateColumn column) {
		return DateCast.asString(column);
	}
	...
}

/**
 * 后续为了可维护性,可以考虑直接使用 apache 的DateFormatUtils.
 * 
 * 迟南已经修复了该问题,但是为了维护性,还是直接使用apache的内置函数
 */
class DateCast {

	static String datetimeFormat = "yyyy-MM-dd HH:mm:ss";

	static String dateFormat = "yyyy-MM-dd";

	static String timeFormat = "HH:mm:ss";

	static String timeZone = "GMT+8";

	static TimeZone timeZoner = TimeZone.getTimeZone(DateCast.timeZone);

	static void init(final Configuration configuration) {
		DateCast.datetimeFormat = configuration.getString(
				"common.column.datetimeFormat", datetimeFormat);
		DateCast.timeFormat = configuration.getString(
				"common.column.timeFormat", timeFormat);
		DateCast.dateFormat = configuration.getString(
				"common.column.dateFormat", dateFormat);
		DateCast.timeZone = configuration.getString("common.column.timeZone",
				DateCast.timeZone);
		DateCast.timeZoner = TimeZone.getTimeZone(DateCast.timeZone);
		return;
	}

	static String asString(final DateColumn column) {
		if (null == column.asDate()) {
			return null;
		}

		switch (column.getSubType()) {
		case DATE:
			return DateFormatUtils.format(column.asDate(), DateCast.dateFormat,
					DateCast.timeZoner);
		case TIME:
			return DateFormatUtils.format(column.asDate(), DateCast.timeFormat,
					DateCast.timeZoner);
		case DATETIME:
			return DateFormatUtils.format(column.asDate(),
					DateCast.datetimeFormat, DateCast.timeZoner);
		default:
			throw DataXException
					.asDataXException(CommonErrorCode.CONVERT_NOT_SUPPORT,
							"时间类型出现不支持类型,目前仅支持DATE/TIME/DATETIME。该类型属于编程错误,请反馈给DataX开发团队 .");
		}
	}
}

五、分析

分析代码可以看出来,hdfswriter底层在做date2String时,使用了一个GMT+8的时区,而mysqlreader并没有使用一个时区,采用的是默认的时区。因此判断出可能和时区有关系,但是首先得确认java默认时区是怎么取得。

默认时区的代码获取可以参考网上的这篇,说的还是比较详细的:JAVA默认时区获取。大致就是jvm参数>linux系统时区>GMT。而datax启动参数里没有配置这个参数,查看了一下linux系统的时区为:Asia/Shanghai。

根据Asia/Shanghai以及GMT+8时区,在网上查找资料后,无意间有一个回答提到了夏令时的问题。因此跟着这个思路看了一下夏令时,发现中国在1986-1991之间实行一段时间的夏令时:每年从四月中旬第一个星期日的凌晨2时整(北京时间),将时钟拨快一小时,即将表针由2时拨至3时,夏令时开始;到九月中旬第一个星期日的凌晨2时整(北京夏令时间),再将时钟拨慢一小时,即将表针由2时拨至1时,夏令时结束。完全就能解释出,为什么指定年份间的指定月份日期会往前推一天。

六、解决方案

1、程序中使用默认的时区去做date2String,去掉GMT+8

2、System.setProperty("user.timezone","GMT+8"); 

3、在datax启动脚本中添加GMT+8时区

Datax-Mysql同步至Hive时时区问题导致日期减少一天_第1张图片

你可能感兴趣的:(Datax)