小知识,大挑战!本文正在参与「程序员必备小知识」创作活动
Map Reduce 之与spark RDD 的一个区别🧐就在于groupby这个操作的实现。
MapReduce 一直以来都宣称它把整个数据的批处理操作高度抽象成了Map和Reduce两个操作(虽然高度抽象这几个字不是很能让人认同😑)。
但是显然,只了解这两个操作是不够的,mapreduce的真正的核心操作其实应该是隐藏在Map和Reduce 过程之间的分组groupby操作,正是这个操作为Map Reduce框架提供了真正意义上的数据处理能力。
与Spark 不同的是,Map Reduce 在分组时会通过先排序再将排序后的数据分组交给Reduce处理(Spark 可以操纵哈希表的方式)。
借助于这个特性,我们可以利用它实现大批量数据的排序。
具体来讲是这样的:Map把我们需要用于排序的字段放置在key处,这样在Map和Reduce的中间阶段,会根据key来排序,然后按key分组。而我们在Reduce阶段只需要把key和相应的每个值都写入上下文即可。
我们来尝试如下示例:
假设文件内容是这些:
33
37
12
40
4
16
39
5
1
45
25
也就是每行由一个数字组成,没有额外的部分,也就是最简单的一种情况。这样的情况下,在Map中我们输入的值就是用来排序的key,而value由于是不存在的,我们可以使用一个空值也就是NullWritable。
Map的实现可以像下面这样
public static class Map extends Mapper<Object, Text, IntWritable, NullWritable> {
private static final IntWritable k = new IntWritable();
@Override
public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
String line = value.toString();
k.set(Integer.parseInt(line));
context.write(k, NullWritable.get());
}
}
这个Map的实现其实有点丑,因为我们没有直接在输入的时候将每行的数据以整数的形式读取,而是当作了文本先读取,于是需要一个整数解析操作。
而在Reduce阶段,我们得到的列表长度实际上就是我们需要输出的次数,而由于我们没有额外的字段,我们可以只输出键或者值的部分即可。不输出的部分使用NullWritable
Reduce 阶段的代码可以像这样
public static class Reduce extends Reducer<IntWritable, NullWritable, IntWritable, NullWritable> {
@Override
public void reduce(IntWritable key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
for (NullWritable ignored : values) {
context.write(key, NullWritable.get());
}
}
}
然后我们设定下相关的Job参数
job.setMapperClass(Map.class);
job.setReducerClass(Reduce.class);
job.setOutputKeyClass(IntWritable.class);
job.setOutputValueClass(NullWritable.class);
FileInputFormat.addInputPath(job, inDir); //输入目录
FileOutputFormat.setOutputPath(job, outDir);//输出目录
在运行本次任务之后,我们可以查看输出的运行结果如下:
1
4
5
12
16
25
33
37
39
40
45
可以看到的是,我们的排序效果已经实现了。
小结:
Map Reduce的 groupby 自带有排序效果,而利用这个特性,我们可以实现对批量数据的排序,而不用关注细节。
这个代码还有相应的改进空间,比如我们可以尝试重写框架的数据读取机制,然后用来适应更多的情况,同时减轻map的负担(例如:map中的解析整数可以去掉),只是它牵扯到了MapReduce的序列化机制,为了不加重我们的心智负担,姑且先以这样的形式完成代码的书写,日后我们做相应的改进。