hadoop中数据文件连接
hadoop实际应用中,经常需要连接来自不同数据源的数据文件,然后在某些属性上进行连接操作。类似于数据库中使用join进行多表连接,例如在Foreign key上进行连接。
在数据库中由于SQL支持join语法,所以实现多表连接只需要写SQL语句即可实现。但是在hadoop中,实现不同数据文件中记录的连接操作,却并没有如此简单。考虑如下两个数据文件。
2,bbbbbbb,5555555555555
4,ddddddd,7777777777777
3,w,12.08,09-Jan-2012
要实现在两在数据文件第一列 customer id上将数据记录进行连接,连接后输出如下:
1,aaaaaaa,4444444444444,y,12.02,04-Mon-2008
result 2,bbbbbbb,5555555555555,z,12.05,05-Oct-2010
3,ccccccc,6666666666666,x,12.01,02-Jun-2009
3,ccccccc,6666666666666,w,12.08,09-Jan-2012
使用MapReduce实现上述操作

- tag 标签 ,由于MapReduce需要处理多个数据源中的数据,所以MapReduce每次处理一条记录的时候需要记住该记录属于哪个数据源,所以每条记录必须打上一个tag
- datasource 数据源 例如上面的 customer ,order
- group key 组键 即连接键,例如上述的customer id
- DataJoinMapperBase 自己实现的Mapper必须继承该类,该类提供了三个抽象方法给子类实现
- DataJoinReducerBase 自己实现的Reducer必须继承的类,需要实现combine方法,完成记录的连接,可以控制是内连接,外连接等等操作
- TaggedMapOutput 记录连接过程中,传递的数据类型,该类似包含了一个Text类型的tag变量,子类需要继承该类,实现getData方法
public class DataJoin extends Configured implements Tool {
public static class MapClass extends DataJoinMapperBase {
@Override
protected Text generateInputTag(String inputFile) { //tag 给记录添加标签,这里以记录所在的文件名称为记录的标签
return new Text(inputFile);
}
@Override
protected TaggedMapOutput generateTaggedMapOutput(Object value) {//将记录原始值包装成 TaggedMapOutput类型
TaggedWritable retv = new TaggedWritable((Text) value);
retv.setTag(this.inputTag); // 上述生generateInputTag方法成的标签被保存在inputTag中
return retv;
}
@Override
protected Text generateGroupKey(TaggedMapOutput aRecord) {//从generateTaggedMapOutput方法生成的每条记录中提取出组键
String line = ((Text) aRecord.getData()).toString();
String[] tokens = line.split(",");
String groupKey = tokens[0];
return new Text(groupKey);
}
}
public static class Reduce extends DataJoinReducerBase {
protected TaggedMapOutput combine(Object[] tags, Object[] values) {//将上述Mapper生成值进行合并
if (tags.length < 2) return null;
String joinedStr = "";
for (int i=0; i<values.length; i++) {
if (i > 0) joinedStr += ",";
TaggedWritable tw = (TaggedWritable) values[i];
String line = ((Text) tw.getData()).toString();
String[] tokens = line.split(",", 2);
joinedStr += tokens[1];
}
TaggedWritable retv = new TaggedWritable(new Text(joinedStr));
retv.setTag((Text) tags[0]);
return retv;
}
}
public static class TaggedWritable extends TaggedMapOutput {
private Writable data;
public TaggedWritable() { //反射 必须要有默认构造方法
this.tag = new Text();
}
public TaggedWritable(Writable data) {
this.tag = new Text("");
this.data = data;
}
public Writable getData() {
return data;
}
public void setData(Writable data) {
this.data = data;
}
public void write(DataOutput out) throws IOException {
this.tag.write(out);
out.writeUTF(this.data.getClass().getName()); //使用序列化 需要传递类名
this.data.write(out);
}
public void readFields(DataInput in) throws IOException {
this.tag.readFields(in);
String dataClz = in.readUTF();
if (this.data == null
|| !this.data.getClass().getName().equals(dataClz)) {
try {
this.data = (Writable) ReflectionUtils.newInstance(
Class.forName(dataClz), null);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
this.data.readFields(in);
}
}
public int run(String[] args) throws Exception {
Configuration conf = getConf();
JobConf job = new JobConf(conf, DataJoin.class);
Path in = new Path(args[0]);
Path out = new Path(args[1]);
FileInputFormat.setInputPaths(job, in);
FileOutputFormat.setOutputPath(job, out);
job.setJobName("DataJoin");
job.setMapperClass(MapClass.class);
job.setReducerClass(Reduce.class);
job.setInputFormat(TextInputFormat.class);
job.setOutputFormat(TextOutputFormat.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(TaggedWritable.class);
job.set("mapred.textoutputformat.separator", ",");
JobClient.runJob(job);
return 0;
}
public static void main(String[] args) throws Exception {
int res = ToolRunner.run(new Configuration(),
new DataJoin(),
args);
System.exit(res);
}
}
下面仔细分析上述代码的实现过程:
public abstract class TaggedMapOutput implements Writable {
protected Text tag;
public TaggedMapOutput() {
this.tag = new Text("");
}
public Text getTag() {
return tag;
}
public void setTag(Text tag) {
this.tag = tag;
}
public abstract Writable getData();
public TaggedMapOutput clone(JobConf job) {
return (TaggedMapOutput) WritableUtils.clone(this, job);
}
}
由于TaggedMapOutput类型需要在集群直接通过网络进行传递,所以实现了Writable接口。所以子类继承时候需要实现getData抽象方法和Writable接口中的方法。个人理解就是,继承该类,
就是将数据文件中的每条记录封装成一个个数据包。数据包包含一个tag和原始的记录值2个部分。
- generateInputTag 方法给整个数据文件定义一个全局的tag,这里习惯将数据源的名称例如 customer作为全局的tag,该tag在后来会被set到每个记录对应的TaggedMapOutput 中,查看DataJoinMapperBase的源代码可以发现,该类使用inputTag变量保存全局的tag。 this.inputTag = generateInputTag(this.inputFile);
- generateTaggedMapOutput方法就是将原始记录封装成数据包(TaggedMapOutput),该方法使用原始记录的值作为输入,返回封装好的数据包。
- generateGroupKey方法使用TaggedMapOutput作为输入,从数据包中取得原始记录值,然后提取出组键值,用户在此次可以根据需要在记录原始值中选取组键值
/**
*
* @param tags
* a list of source tags
* @param values
* a value per source
* @return combined value derived from values of the sources
*/
protected abstract TaggedMapOutput combine(Object[] tags, Object[] values);
public void map(Object arg0, Object arg1, OutputCollector arg2,
Reporter arg3) throws IOException {
// TODO Auto-generated method stub
}
在combine方法中,我需要做如下工作,筛选不需要的组合,执行联接操作(外连接,内连接),将联接的结果进行合并以合适的格式进行输出。在两个数据源情况下,合并的记录数要么2个,要么1个。从combine方法的签名中可以看到,参数为一个标签数组和一个值数组。由于一个值对应一个标签,所以这两个数组的大小应该是一样的。以本为开始数据文件为输入,某次combine调用,tag数组和value数组内容如下: