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类型的数据不会有这种问题,对于这种现象令人十分迷惑,只能先从代码上找找原因所在,但可以确定大概率的问题应该出在类型转换上。
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时区