目录
MapReduce中的分组
1 默认分组
2 自定义分组
3 可能存在的问题
4 总结
MapReduce中的分组
分组组件是Map端至Reduce端过程中的一部分,即:Map -----> 分组组件 ------> Reduce;
1 默认分组
默认情况下:
Reduce端接收到的数据是按照map输出的key进行分组;分组时,Key相同的为一组;Key中的对象,均实现了WritableComparable接口。
当没有自定义分组时,即:默认分组;就会自动去找接口中的compareTo()方法。该方法返回值是0的,分为为一组;返回值不为的0,则分到下一组;
比如:在WordCount程序中,map输出的Key是单词,我们就是想让相同的单词为一组,即分组字段就是map输出的Key。
但如果我想要的分组,并不是Key中的全部字段,这该怎么办呢?
比如:我现在想统计各年级学生的成绩,而年级字段是:3年3班,3年2班等。若还按照年级为key发送,则会是3年3班的数据在一起,3年2班的数据在一起。而不是按年级划分、分组的。
若只想要年级字段,则需要自定义分组。
再比如,课程分数统计面试题中,有这样的一个需求:求出每门课程中、学生成绩平均分最高学生的信息。
解题思路:
这道题的实质是:分组求最大值,只要处理好两件事情即可实现。一是分组,二是排序(排序要巧妙的运用shuffle过程)。
若想在Shuffle过程中进行排序,则需要让排序字段为key;
①若map端发出的key是分数;会造成相同的分数调用一次reduce方法,无法保证相同分数是同一门课程。越来越偏离需求。
②若map端发出的key是科目;则需要在reduce端求最大值,这容易产生数据倾斜,所以这不是最佳方案。
最佳方案为:map发出的key是:科目和分数;即:按分数排序,按科目分组。
若以科目和分数同时作为key,默认情况下必然会造成,相同科目和相同分数的数据汇总到一组中。
所以应该自定义分组!!!
2 自定义分组
以自定义分组的方式解决上面的分数统计问题。设计MapReduce,其中:
map:拆分课程和分数字段,并封装成对象(用于自定义排序)作为key进行发送。value:姓名。
reduce:发送到Reduce端的数据已经是分好组、排好序的了,所以reduce依旧是统计工作。
分组说白了就是比较的过程,按照key比较,只要返回值是0的就会认为是一组;不返回0,就化分到下一组中。
自定义分组流程:
①创建自定义分组类,并继承WritableComparator类;
②重写compare()方法;
public class MyGroup extends WritableComparator{
public MyGroup() {
// 默认情况下,第二个参数是false,不会构建自定义的实例对象,需要手动修改为true
super(ScoreBean.class,true);
}
public int compare(WritableComparable a, WritableComparable b) {
ScoreBean ascBean = (ScoreBean)a;
ScoreBean bscBean = (ScoreBean)b;
// 只关心返回0的值
return ascBean.getCourse().compareTo(bscBean.getCourse()) ;
}
}
③在驱动类中添加自定义分组;
job.setGroupingComparatorClass(MyGroup.class)
3 可能存在的问题
课程分数统计最终想要的结果是:按科目分好组之后,在组内对分数进行排序。但按照之前的写法会输出类似以下的结果:
从这个结果可以看出,自定义分组有作用,但没有完全成功。分组位置不对,产生了先排序,后分组的效果。这是怎么回事呢?
实际上,在map到reduce过程中,默认的是先排序,后分组。所以在重写排序函数compareTo()时,需将分组字段纳入排序范围,这样就会将相同的字段值排在一起。比如:先按课程排序,再按分数排序。
需要将原来排序的代码进行修改,如下所示:
原排序代码:
/**
* 定义排序规则
* 按照分数降序排列
* 产生先排序,后分组的效果
*/
@Override
public int compareTo(ScoreBean o) {
return o.getScore()-this.getScore()>0?1:(o.getScore()-this.getScore()<0?-1:0);
}
修改后的代码:
/**
* 定义排序规则
* 按照分数降序排列
*/
@Override
public int compareTo(ScoreBean o) {
// 先进行课程比较,目的是到reduce端,相同的课程在排一起
int tmp = o.getCourse().compareTo(this.getCourse());
if(tmp == 0) {
// 课程相同,排分数
return o.getScore()-this.getScore()>0?1:(o.getScore()-this.getScore()<0?-1:0);
}
return tmp;
}
4 总结
(1)当既有分组字段又有排序字段时,分组字段一定是排序字段的前几个。
因为MapReduce是排序在前,分组在后,所以,排序字段中一定要包含分组字段,且分组字段的排序要写在排序字段的前面。
比如:排序:A B C; 分组:D F
实际上,排序方法中的字段顺序应该是:D F A B C;分组字段是:D F
这是为了保证相同的分组数据相邻。
实际上的分组,仅仅是将map输出的结果,相邻的数据进行比较。即仅仅会比较前一条数据和后一条数据。如果返回相同的值,则为一组;如果不是,重新划分组。所以,若想分组,则一定要能够保证分组字段的数据在相邻的位置。
(2)Reduce中的Iterable<Text> values 只能循环遍历一次;每次循环遍历完指针都会移动到最后一个。
(3)Reduce中的迭代器中所有对象共用同一个地址;