MapReduce之Join操作(Reduce-side Join)

本文介绍Hadoop中RepartitionJoin的基本原理及其实现细节,包括MapReduce过程中如何通过设置键值进行数据分组,以及如何在reduce阶段完成join操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在关系型数据库中 join 是非常常见的操作,各种优化手段已经到了极致。在海量数据的环境下,不可避免的也会碰到这种类型的需求,例如在数据分析时需要连接从不同的数据源中获取到的数据。不同于传统的单机模式,在分布式存储的下采用 MapReduce 编程模型,也有相应的处理措施和优化方法。

本文对 Hadoop 中最基本的 join 方法进行简单介绍,这也是其它许多方法和优化措施的基础。文中所采用的例子来自于《 Hadoop in Action 》一书中的 5.2 节 。假设两个表所在的文件分别为Customers和Orders,以CSV格式存储在HDFS中。

1,Stephanie Leung,555-555-5555
2,Edward Kim,123-456-7890
3,Jose Madriz,281-330-8004
4,David Stork,408-555-0000

3,A,12.95,02-Jun-2008
1,B,88.25,20-May-2008

2,C,32.00,30-Nov-2007
3,D,25.02,22-Jan-2009

这里的Customer ID是连接的键,那么连接的结果:

1,Stephanie Leung,555-555-5555,B,88.25,20-May-2008
2,Edward Kim,123-456-7890,C,32.00,30-Nov-2007
3,Jose Madriz,281-330-8004,A,12.95,02-Jun-2008
3,Jose Madriz,281-330-8004,D,25.02,22-Jan-2009

      回忆一下Hadoop中MapReduce中的主要几个过程:依次是读取数据分块,map操作,shuffle操作,reduce操作,然后输出结果。简单来说,其本质在于大而化小,分拆处理。显然我们想到的是将两个数据表中键值相同的元组放到同一个reduce结点进行,关键问题在于如何做到?具体处理方法是将map操作输出的key值设为两表的 连接键(如例子中的Customer ID) ,那么在shuffle阶段,Hadoop中默认的partitioner会将相同key值得map输出发送到同一个reduce结点。所以整个过程如下图所示:


        这种方法称为Repartition Join,同时它进行join操作是在reduce阶段进行,也属于Reduce-side Join;在Hadoop中contrib目录下的datajoin就是采用的这种方法。



       上面介绍了 Repartition Join 的基本思想,实践出真知,具体的实现中总是存在各种细节问题。下面我们通过具体的源码分析来加深理解。本文分析的是 Hadoop-0.20.2 版本的 datajoin 代码,其它版本也许会有变化,这里暂且不论。

参看源码目录下,共实现有 7 个类,分别是:

  • ArrayListBackIterator.java
  • DataJoinJob.java
  • DataJoinMapperBase.java
  • DataJoinReducerBase.java
  • JobBase.java
  • ResetableIterator.java
  • TaggedMapOutput.java

        源码比较简单,代码量小,下面对一些关键的地方进行分析:前面我们提到了 map 阶段的输出的 key 值的设定;然而在实现中,其value值也是另外一个需要考虑的地方,在不同的 reduce 结点进行 join 操作时,需要知道参与 join 的元组所属的表;解决方法是在 map 输出的 value 值中加入一个标记 (tag) ,例如上一篇例子中两表的tag 可以分别 customer 和 order (注:实际上,在reduce阶段可以直接分析两元组的结构就可以确定数据来源)。这也是 TaggedMapOutput.java 的来历。作为 Hadoop 的中间数据,必须实现 Writable 的方法,如下所示:

Java代码  收藏代码
  1. public abstract class TaggedMapOutput implements Writable {  
  2.     protected Text tag;  
  3.     public TaggedMapOutput() {  
  4.         this.tag = new Text("");  
  5.     }  
  6.     public Text getTag() {  
  7.         return tag;  
  8.     }  
  9.     public void setTag(Text tag) {  
  10.         this.tag = tag;  
  11.     }  
  12.     public abstract Writable getData();    
  13.     public TaggedMapOutput clone(JobConf job) {  
  14.         return (TaggedMapOutput) WritableUtils.clone(this, job);  
  15.     }  
  16. }   

接下来,我们看看 DataJoinMapperBase 中的相关方法

Java代码  收藏代码
  1. protected abstract TaggedMapOutput generateTaggedMapOutput(Object value);  
  2. protected abstract Text generateGroupKey(TaggedMapOutput aRecord);  

以上两个方法需要由子类实现。上一篇文章提到,将两个表的连接键作为 map 输出的 key 值,其中第二个方法所做的就是这件事,生成一个类型为 Text 的 key ,不过这里是将它称作是 GroupKey 而已。因此 map 方法也就比较简单易懂了

Java代码  收藏代码
  1. public void map(Object key, Object value, OutputCollector output,   
  2.                          Reporter reporter) throws IOException {  
  3.     if (this.reporter == null) {  
  4.         this.reporter = reporter;  
  5.     }  
  6.     addLongValue("totalCount"1);  
  7.     TaggedMapOutput aRecord = generateTaggedMapOutput(value);  
  8.     if (aRecord == null) {  
  9.         addLongValue("discardedCount"1);  
  10.         return;  
  11.     }  
  12.     Text groupKey = generateGroupKey(aRecord);  
  13.     if (groupKey == null) {  
  14.         addLongValue("nullGroupKeyCount"1);  
  15.         return;  
  16.     }  
  17.     output.collect(groupKey, aRecord);  
  18.     addLongValue("collectedCount"1);  
  19. }  

说完了 map 操作,接下来就是 reduce 阶段的事情了。参看 DataJoinReducerBase 这个类,其中的 reduce 方法主要部分是:

Java代码  收藏代码
  1. public void reduce(Object key, Iterator values,   
  2.                              OutputCollector output, Reporter reporter) throws IOException {  
  3.   
  4.     if (this.reporter == null) {  
  5.         this.reporter = reporter;  
  6.     }  
  7.   
  8.     SortedMap<Object, ResetableIterator> groups = regroup(key, values, reporter);  
  9.   
  10.      Object[] tags = groups.keySet().toArray();  
  11.   
  12.     ResetableIterator[] groupValues = new ResetableIterator[tags.length];  
  13.   
  14.     for (int i = 0; i < tags.length; i++) {  
  15.         groupValues[i] = groups.get(tags[i]);  
  16.     }  
  17.   
  18.     joinAndCollect(tags, groupValues, key, output, reporter);  
  19.     addLongValue("groupCount"1);  
  20.   
  21.     for (int i = 0; i < tags.length; i++) {  
  22.         groupValues[i].close();  
  23.     }  
  24. }  

其中 groups 数组保存的是 tag 以及它们对应元组的 iterator 。例如 Customer ID 为 3 的数据块所在的 reduce 节点上, tags = {"Custmoers" , "Orders"}, groupValues 则对应 {"3,Jose Madriz,281-330-8004"} 和 {"3,A,12.95,02-Jun-2008","3,D,25.02,22-Jan-2009"} 的 iterator 。归根结底,关于两个元组的 join 操作放在

Java代码  收藏代码
  1. protected abstract TaggedMapOutput combine(Object[] tags, Object[] values);  

该方法由子类实现。

下面附上 《 Hadoop in Action 》中提供的一种实现

Java代码  收藏代码
  1. public class DataJoin extends Confi gured implements Tool {  
  2.       
  3.     public static class MapClass extends DataJoinMapperBase {  
  4.         protected Text generateInputTag(String inputFile) {  
  5.             String datasource = inputFile.split(“-”)[0];  
  6.             return new Text(datasource);  
  7.         }  
  8.         protected Text generateGroupKey(TaggedMapOutput aRecord) {  
  9.             String line = ((Text) aRecord.getData()).toString();  
  10.             String[] tokens = line.split(“,”);  
  11.             String groupKey = tokens[0];  
  12.             return new Text(groupKey);  
  13.         }  
  14.         protected TaggedMapOutput generateTaggedMapOutput(Object value) {  
  15.            TaggedWritable retv = new TaggedWritable((Text) value);  
  16.            retv.setTag(this.inputTag);  
  17.            return retv;  
  18.        }  
  19.     }  
  20.       
  21.     public static class Reduce extends DataJoinReducerBase {  
  22.         protected TaggedMapOutput combine(Object[] tags, Object[] values) {  
  23.             if (tags.length < 2return null;  
  24.             String joinedStr = “”;  
  25.             for (int i=0; i<values.length; i++) {  
  26.                 if (i > 0) joinedStr += “,”;  
  27.                 TaggedWritable tw = (TaggedWritable) values[i];  
  28.                 String line = ((Text) tw.getData()).toString();  
  29.                 String[] tokens = line.split(“,”, 2);  
  30.                 joinedStr += tokens[1];  
  31.             }  
  32.             TaggedWritable retv = new TaggedWritable(new Text(joinedStr));  
  33.             retv.setTag((Text) tags[0]);  
  34.             return retv;  
  35.         }  
  36.     }  
  37.       
  38.     public static class TaggedWritable extends TaggedMapOutput {  
  39.         private Writable data;  
  40.         public TaggedWritable(Writable data) {  
  41.             this.tag = new Text(“”);  
  42.             this.data = data;  
  43.         }  
  44.         public Writable getData() {  
  45.             return data;  
  46.         }  
  47.         public void write(DataOutput out) throws IOException {  
  48.             this.tag.write(out);  
  49.             this.data.write(out);  
  50.         }     
  51.         public void readFields(DataInput in) throws IOException {  
  52.             this.tag.readFields(in);  
  53.             this.data.readFields(in);  
  54.         }  
  55.     }  
  56.       
  57.     public int run(String[] args) throws Exception {  
  58.         Confi guration conf = getConf();  
  59.         JobConf job = new JobConf(conf, DataJoin.class);  
  60.         Path in = new Path(args[0]);  
  61.         Path out = new Path(args[1]);  
  62.         FileInputFormat.setInputPaths(job, in);  
  63.         FileOutputFormat.setOutputPath(job, out);  
  64.         job.setJobName(“DataJoin”);  
  65.         job.setMapperClass(MapClass.class);  
  66.         job.setReducerClass(Reduce.class);  
  67.         job.setInputFormat(TextInputFormat.class);  
  68.         job.setOutputFormat(TextOutputFormat.class);  
  69.         job.setOutputKeyClass(Text.class);  
  70.         job.setOutputValueClass(TaggedWritable.class);  
  71.         job.set(“mapred.textoutputformat.separator”, “,”);  
  72.         JobClient.runJob(job);  
  73.         return 0;  
  74.     }  
  75.       
  76.     public static void main(String[] args) throws Exception {  
  77.         int res = ToolRunner.run(new Confi guration(),  
  78.         new DataJoin(),  
  79.         args);  
  80.         System.exit(res);  
  81.     }  
  82.   
  83. }   
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值