上一篇博客说明了怎么自定义Key,而且用了二次排序的例子来做测试,但没有详细的说明二次排序,这一篇说详细的说明二次排序,为了说明曾经一个思想的误区,特地做了一个3个字段的二次排序来说明。后面称其为“三次排序”。
测试数据:
a1,b2,c5
a4,b1,c3
a1,b2,c4
a2,b2,c4
a2,b1,c4
a4,b1,c2
测试目的:输出以下结果首先根据第一个自段排序,如果第一个字段排好后再根据第二个字段的升序排序最后在根据第三个字段进行排序,得到以下结果。
a1 b2,c4
a1 b2,c5
a2 b1,c4
a2 b2,c4
a4 b1,c2
a4 b1,c3
之所以会设置三个字段,是想说明一个曾经困扰我很久的问题,问题大概描述如下:以下网上学习某大神讲的mapreduce时他对二次排序原理的一段描述
二次排序原理
在map阶段,使用job.setInputFormatClass定义的InputFormat将输入的数据集分割成小数据块splites,同时InputFormat提供一个RecordReder的实现。本例子中使用的是TextInputFormat,他提供的RecordReader会将文本的字节偏移量作为key,这一行的文本作为value。这就是自定义Map的输入是<LongWritable, Text>的原因。然后调用自定义Map的map方法,将一个个<LongWritable, Text>对输入给Map的map方法。注意输出应该符合自定义Map中定义的输出<IntPair, IntWritable>。最终是生成一个List<IntPair, IntWritable>。
排序的过程:(当时理解的第一次排序,只排序自定义类型中的第一个字段)
在map阶段的最后,会先调用job.setPartitionerClass对这个List进行分区,每个分区映射到一个reducer。每个分区内又调用job.setSortComparatorClass设置的key比较函数类排序。可以看到,这本身就是一个二次排序。 如果没有通过job.setSortComparatorClass设置key比较函数类,则使用key的实现的compareTo方法。
排序的过程:(当时理解的第二次排序,排序自定义类型中的第二个字段)
在reduce阶段,reducer接收到所有映射到这个reducer的map输出后,也是会调用job.setSortComparatorClass设置的key比较函数类对所有数据对排序。然后开始构造一个key对应的value迭代器。这时就要用到分组,使用job.setGroupingComparatorClass设置的分组函数类( 如果没设置则会判断是否Key中所有的字段是否都相同,比较整个对象流的字节)。只要这个比较器比较的两个key相同,他们就属于同一个组,它们的value放在一个value迭代器,而这个迭代器的key使用属于同一个组的所有key的第一个key。最后就是进入Reducer的reduce方法,reduce方法的输入是所有的(key和它的value迭代器)。同样注意输入与输出的类型必须与自定义的Reducer中声明的一致。
核心总结:
1、map最后阶段进行partition分区,一般使用job.setPartitionerClass设置的类,如果没有自定义Key的hashCode()方法进行分区。
2、每个分区内部调用job.setSortComparatorClass设置的key的比较函数类进行排序,如果没有则使用Key的实现的compareTo方法。
3、当reduce接收到所有map传输过来的数据之后,调用job.setSortComparatorClass设置的key比较函数类对所有数据对排序,如果没有则使用Key的实现的compareTo方法。
4、紧接着使用job.setGroupingComparatorClass设置的分组函数类,进行分组,同一个Key的value放在一个迭代器里面。如果未指定GroupingComparatorClass则则使用Key的实现的compareTo方法来对其分组。
以下是我刚开始思考的二次排序的数据流时的错误的想法:如果是多个map对同一个reduce发数据:数据流不就成为下面这样了
a4,b4 a3,b3
a3,b3 a4,b4
---------------------->
a2,b1 a1,b2
a1,b2 a2,b1
如果reduce 接收到上一整块数据,那么hadoop框架便会在对第一个字段排序,什么时候对第二个字段排的序呢,但为什么结果又是排过序的呢
所以我做了本例的实验,用三个字段来排序,实验的结果就是本例的结果,最后还是感叹自己学艺不精啊,排序的本质不就是根据compareTo做完整的比做吗
所以从map端到reduce端的数据应该是这样的:已经做一次完整的排序了
a4,b4 a3,b3
a3,b3 a4,b4
---------------------->
a2,b1 a1,b1
a1,b2 a2,b2
了解了二次排序的原理后,那么下面我们就开始实现以上功能:
测试数据:
a1,b2,c5
a4,b1,c3
a1,b2,c4
a2,b2,c4
a2,b1,c4
a4,b1,c2
测试目的:输出以下结果首先根据第一个自段排序,如果第一个字段排好后再根据第二个字段的升序排序最后在根据第三个字段进行排序,得到以下结果。
a1 b2,c4
a1 b2,c5
a2 b1,c4
a2 b2,c4
a4 b1,c2
a4 b1,c3
之所以会设置三个字段,是想说明一个曾经困扰我很久的问题,问题大概描述如下:以下网上学习某大神讲的mapreduce时他对二次排序原理的一段描述
二次排序原理
在map阶段,使用job.setInputFormatClass定义的InputFormat将输入的数据集分割成小数据块splites,同时InputFormat提供一个RecordReder的实现。本例子中使用的是TextInputFormat,他提供的RecordReader会将文本的字节偏移量作为key,这一行的文本作为value。这就是自定义Map的输入是<LongWritable, Text>的原因。然后调用自定义Map的map方法,将一个个<LongWritable, Text>对输入给Map的map方法。注意输出应该符合自定义Map中定义的输出<IntPair, IntWritable>。最终是生成一个List<IntPair, IntWritable>。
排序的过程:(当时理解的第一次排序,只排序自定义类型中的第一个字段)
在map阶段的最后,会先调用job.setPartitionerClass对这个List进行分区,每个分区映射到一个reducer。每个分区内又调用job.setSortComparatorClass设置的key比较函数类排序。可以看到,这本身就是一个二次排序。 如果没有通过job.setSortComparatorClass设置key比较函数类,则使用key的实现的compareTo方法。
排序的过程:(当时理解的第二次排序,排序自定义类型中的第二个字段)
在reduce阶段,reducer接收到所有映射到这个reducer的map输出后,也是会调用job.setSortComparatorClass设置的key比较函数类对所有数据对排序。然后开始构造一个key对应的value迭代器。这时就要用到分组,使用job.setGroupingComparatorClass设置的分组函数类( 如果没设置则会判断是否Key中所有的字段是否都相同,比较整个对象流的字节)。只要这个比较器比较的两个key相同,他们就属于同一个组,它们的value放在一个value迭代器,而这个迭代器的key使用属于同一个组的所有key的第一个key。最后就是进入Reducer的reduce方法,reduce方法的输入是所有的(key和它的value迭代器)。同样注意输入与输出的类型必须与自定义的Reducer中声明的一致。
核心总结:
1、map最后阶段进行partition分区,一般使用job.setPartitionerClass设置的类,如果没有自定义Key的hashCode()方法进行分区。
2、每个分区内部调用job.setSortComparatorClass设置的key的比较函数类进行排序,如果没有则使用Key的实现的compareTo方法。
3、当reduce接收到所有map传输过来的数据之后,调用job.setSortComparatorClass设置的key比较函数类对所有数据对排序,如果没有则使用Key的实现的compareTo方法。
4、紧接着使用job.setGroupingComparatorClass设置的分组函数类,进行分组,同一个Key的value放在一个迭代器里面。如果未指定GroupingComparatorClass则则使用Key的实现的compareTo方法来对其分组。
以下是我刚开始思考的二次排序的数据流时的错误的想法:如果是多个map对同一个reduce发数据:数据流不就成为下面这样了
a4,b4 a3,b3
a3,b3 a4,b4
---------------------->
a2,b1 a1,b2
a1,b2 a2,b1
如果reduce 接收到上一整块数据,那么hadoop框架便会在对第一个字段排序,什么时候对第二个字段排的序呢,但为什么结果又是排过序的呢
所以我做了本例的实验,用三个字段来排序,实验的结果就是本例的结果,最后还是感叹自己学艺不精啊,排序的本质不就是根据compareTo做完整的比做吗
所以从map端到reduce端的数据应该是这样的:已经做一次完整的排序了
a4,b4 a3,b3
a3,b3 a4,b4
---------------------->
a2,b1 a1,b1
a1,b2 a2,b2
了解了二次排序的原理后,那么下面我们就开始实现以上功能:
定义一个自定义的数据类型:里面的public int compareTo(ThirdSortClass o)方法是"三次排序的关键"
自定义Key:
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.WritableComparable;
public class ThirdSortClass implements WritableComparable<ThirdSortClass> {
/**
* 自定义类型的中包含的变量,本例中的变量都是用于排序的变量
* 后序的事例中我们还将定义一些其它功能的变量
*/
private String first;
private String second;
private String third;
public ThirdSortClass() {}
public ThirdSortClass(String first, String second, String third) {
this.first = first;
this.second = second;
this.third = third;
}
/**
* 反序列化,从流中的二进制转换成自定义Key
*/
@Override
public void readFields(DataInput input) throws IOException {
this.first = input.readUTF();
this.second = input.readUTF();
this.third = input.readUTF();
}
/**
* 序列化,将自定义Key转化成使用流传送的二进制
*/
@Override
public void write(DataOutput output) throws IOException {
output.writeUTF(first);
output.writeUTF(second);
output.writeUTF(third);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((first == null) ? 0 : first.hashCode());
result = prime * result + ((second == null) ? 0 : second.hashCode());
result = prime * result + ((third == null) ? 0 : third.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ThirdSortClass other = (ThirdSortClass) obj;
if (first == null) {
if (other.first != null)
return false;
} else if (!first.equals(other.first))
return false;
if (second == null) {
if (other.second != null)
return false;
} else if (!second.equals(other.second))
return false;
if (third == null) {
if (other.third != null)
return false;
} else if (!third.equals(other.third))
return false;
return true;
}
/**
* 用于map阶段和reduce阶段的排序,以及reduce阶段的grouping分组
* 这里是二次排序的关键,二次排序功能的实现主要就在这一个方法
*/
@Override
public int compareTo(ThirdSortClass o) {
if(!this.first.equals(o.getFirst())) {
return this.first.compareTo(o.getFirst());
} else if (!this.second.equals(o.getSecond())) {
return this.second.compareTo(o.getSecond());
} else if (!this.third.equals(o.getThird())) {
return this.third.compareTo(o.getThird());
}
return 0;
}
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
public String getSecond() {
return second;
}
public void setSecond(String second) {
this.second = second;
}
public String getThird() {
return third;
}
public void setThird(String third) {
this.third = third;
}
}
map阶段:
import java.io.IOException;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
public class ThirdMapper extends Mapper<LongWritable, Text, ThirdSortClass, Text> {
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
String line = value.toString().trim();
if(line.length() > 0) {
String[] arr = line.split(",");
if(arr.length == 3) {
context.write(new ThirdSortClass(arr[0],arr[1], arr[2]), new Text(arr[1] + "," + arr[2]));
}
}
}
}
reduce阶段:
import java.io.IOException;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
public class ThirdSortReducer extends Reducer<ThirdSortClass, Text, Text, Text> {
private Text Okey = new Text();
@Override
protected void reduce(ThirdSortClass key, Iterable<Text> values, Context context)
throws IOException, InterruptedException {
// for(Text val: values) {
// context.write(new Text(key.getFirst()), val);
// }
Okey.set(key.getFirst());
context.write(Okey, values.iterator().next());
}
}
启动函数:
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class JobMain {
public static void main(String[] args) throws Exception{
Configuration configuration = new Configuration();
Job job = new Job(configuration, "third-sort-job");
job.setJarByClass(JobMain.class);
job.setMapperClass(ThirdMapper.class);
job.setMapOutputKeyClass(ThirdSortClass.class);
job.setMapOutputValueClass(Text.class);
/**
* 这里没有用partitioner,到目前为止我们都是做的一个reduce的测试,
* 到后面说明全局排序时,还会着重说明多个reduce的写法。
* 后面会有一个实例专门说明partitioner的定制,到说明求奇偶数行之和时会用到
*/
// job.setPartitionerClass(ThirdSortPatitioner.class);
job.setReducerClass(ThirdSortReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
/**
* 为避免博客过长,这里也不说明setGroupingComparatorClass()方法的使用,因为
* 用到这个比较后,reduce的写法会有一些区别,后面有专门的一篇博客说明什么时候
* 用这个方法,以及mapreduce的中reduce的分组过程是会做比较
*/
// job.setGroupingComparatorClass(ThirdSortGroupingComparator.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
Path outputDir = new Path(args[1]);
FileSystem fs = FileSystem.get(configuration);
if(fs.exists(outputDir)) {
fs.delete(outputDir, true);
}
FileOutputFormat.setOutputPath(job, outputDir);
System.exit(job.waitForCompletion(true)? 0: 1);
}
}
运行结果:
总结:
在这一篇博客中留下一个问题,就是整个流程的排序是都是用的key的compareTo()方法,但是不是所有的情况都能这样做的,而且规范一点的写法也是单独的各自实现自己的比较器,这里是为了文章的简洁,突出要说明的某一个点,而不让其它的因素影响对要讲解的点的说明。但未说明的地方,后面的博客中都会做为一个技能点来说明。下一篇就说明如何定制GroupingComparatorClass和SortComparatorClass。