MapReduce编程:3. 航班数据按月份升序,星期降序排列
题目
- 航空公司数据按月份升序,同时按星期降序排列
- 任意数量的Reducer
- 输出文件全排序:Part-0000,Part-0001,…按文件名依次接将得到一个完整的结果排序
- 输入一下字段逗号隔开:月份,星期几,起飞机场,目的机场
分析
- 题目要求只要实现排序,所以不存在计算的问题,但是要使用两个字段来排序,所以需要自己定义数据类型,来作为map输出的键,而后的起飞机场和目的机场也是需要自己定义数据类型的,所以首先需要了解Writable接口和WritableComparable接口
- 在 https://blog.csdn.net/Dongroot/article/details/88571680 这篇博客中,我输出了一次中间的处理结果,可以看到,hadoop对自带的类是有默认排序的,所以对于自定义的类,hadoop肯定无法默认排序了,所以需要自己定义排序规则
- 在自定义的数据类型可以进行排序之后,hadoop会根据排序规则去进行排序,这时map和reduce不在需要做别的处理,只需要在map将数据分成键值对,在reduce将结果输出,但是我们要使用多reduce对排序进行并行处理,所以还需要了解Partitioner类
Writable接口和WritableComparable接口
- 在hadoop中,集群环境下,处理数据是在不同的计算机,机器间的数据流通必须要通过网络,所以hadoop提供了Writable接口,来实现数据的序列化与反序列化,这个原理我知道,但是说不清楚,就和做javaweb开发的时候,Javabean要实现Serializable接口一样的道理。所以只要记住,要自己定义数据类型,必须要实现Writable接口
- 同时,MapReduce的输入输出都是键值对形式的,在shuffle阶段,会对键值对根据键的数据类型,进行排序,所以作为键的类,还必须要实现Comparable接口,来定义排序规则
- 所以hadoop提供了WritableComparable接口,实现这个接口,相当于同时实现了Writable和Comparable接口
- Writable接口有两个方法,readFields(DataInput in)和write(DataOutput out),前者是从输入流中抓取数据,重新创建Writable实例,DataInput中的方法可以反序列化java的基本类型;后者是将实例写入到输出流中,DataOutput中的方法可以序列化java的基本类型
- WritableComparable接口多了一个CompareTo(Object o)方法,作用就是定义排序规则,将调用者与参数进行对比,返回值为int型,大于返回大于零的数,等于返回0,小于返回小于零的数
- 还看不懂的话,就没办法了,推荐个博客
https://blog.csdn.net/chengyuqiang/article/details/78634755
Partitioner类
- 在前面的两个博客中,我都只使用了一个reduce进行操作,很显然没有发挥出MapReduce的全部实力,所以这次我们使用多个reduce进行并行处理,那么问题来了,每个reduce会产生一个输出文件,那么hadoop是如何知道map的结果应该发往哪个reduce进行处理,这就需要指定分区
- hadoop自带了几个Partitioner,在不指定的情况下,hadoop默认使用HashPartitioner,根据哈希算法计算分区,还有一种是TotalOrderPartitioner,分区中的数据范围需要通过分区文件来确认
- 对于TotalOrderPartitioner,分区文件可以自己手动创建,进行等距划分,但是如果数据本身是分布不均匀的,会导致有些reduce工作量大,有些工作量小,所以还可以进行数据抽样,然后根据抽样结果的数据分布,创建分区文件
- 除了这两种Partitioner,还可以自己定义,只要继承Partitioner类,实现getPartition方法,该方法返回值为int型,表示发往第几个reduce,从0开始
- 推荐个博客,我只能解释到这了
https://blog.csdn.net/baolibin528/article/details/50801604
准备工作
- ubuntu14环境
- 启动的hadoop
- 安装hadoop插件的eclipse
- 一个原始数据集
对于前三个,还没弄好的话就去看我之前的博客,这次的原始数据集只需要四列数据,如图:
分别是月份,星期,起飞机场,目的机场,我们需要关注的只是前两列数据,数据之间用逗号隔开。
文件创建好后,上传到HDFS,然后打开eclipse,创建MapReduce Project
上代码
- 首先创建我们自己定义的键的数据类型
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.WritableComparable;
public class OutputKey implements WritableComparable<OutputKey> {
// 月份
private String month;
// 星期
private String week;
private static final List<String> WEEKS = new ArrayList<String>(){
// 使用内部类进行初始化
{
this.add("Mon");
this.add("Tues");
this.add("Wed");
this.add("Thur");
this.add("Fri");
this.add("Sat");
this.add("Sun");
}
};
public OutputKey(){
}
public OutputKey(String m , String w){
month = m;
week = w;
}
public String getMonth() {
return month;
}
public void setMonth(String month) {
this.month = month;
}
public String getWeek() {
return week;
}
public void setWeek(String week) {
this.week = week;
}
@Override
public void readFields(DataInput in) throws IOException {
Text text = new Text();
text.readFields(in);
String[] keys = text.toString().split("\t");
this.month = keys[0];
this.week = keys[1];
}
@Override
public void write(DataOutput out) throws IOException {
Text text = new Text(month+"\t"+week);
text.write(out);
}
@Override
public int compareTo(OutputKey o) {
if(this.month.compareTo(o.getMonth()) > 0) {
return 1;
}
else if(this.month.compareTo(o.getMonth()) < 0) {
return -1;
}
else if(WEEKS.indexOf(this.week) > WEEKS.indexOf(o.getWeek())){
return -1;
}
else if(WEEKS.indexOf(this.week) < WEEKS.indexOf(o.getWeek())){
return 1;
}
return 0;
}
@Override
public String toString() {
return month + "\t" + week ;
}
}
解释一个地方,在readFields和write方法里面,我借助了Text类的方法,对输入输出流进行操作,因为Text类是hadoop自带的已经实现了Writable接口的类,所以封装好的东西为什么还要自己再重新写一遍,自己写也可以,不嫌麻烦就自己写吧
- 然后创建我们自己定义的值的数据类型
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.Writable;
public class OutputValue implements Writable {
// 起飞机场
private String start;
// 目的机场
private String end;
public OutputValue() {
}
public OutputValue(String s,String e) {
this.start = s;
this.end = e;
}
public String getStart() {
return start;
}
public void setStart(String start) {
this.start = start;
}
public String getEnd() {
return end;
}
public void setEnd(String end) {
this.end = end;
}
@Override
public void readFields(DataInput in) throws IOException {
Text text = new Text();
text.readFields(in);
String[] keys = text.toString().split("\t");
this.start = keys[0];
this.end = keys[1];
}
@Override
public void write(DataOutput out) throws IOException {
Text text = new Text(start+"\t"+end);
text.write(out);
}
@Override
public String toString() {
return start + "\t" + end;
}
}
- 然后创建Mapper类
import java.io.IOException;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
public class MyMapper extends Mapper<Object, Text, OutputKey, OutputValue> {
@Override
protected void map(Object key, Text value, Mapper<Object, Text, OutputKey, OutputValue>.Context context)
throws IOException, InterruptedException {
/*
* map输入:
* 原始文本
*
* map输出:
* key2,(月份,星期)
* value2,(起飞地,目的地)
*/
String[] values = value.toString().split(",");
context.write(new OutputKey(values[0], values[1]), new OutputValue(values[2],values[3]));
}
}
- 创建Reducer类
import java.io.IOException;
import org.apache.hadoop.mapreduce.Reducer;
public class MyReducer extends Reducer<OutputKey, OutputValue , OutputKey, OutputValue> {
@Override
protected void reduce(OutputKey outputKey, Iterable<OutputValue> iterable, Reducer<OutputKey, OutputValue, OutputKey, OutputValue>.Context context)
throws IOException, InterruptedException {
/*
* reduce输入:
* key2,(月份,星期)
* value2,(起飞地,目的地)
*
* reduce输出:
* 无
*/
for(OutputValue outputValue: iterable) {
context.write(outputKey, outputValue);
}
}
}
- 接下来创建Partitioner类
import org.apache.hadoop.mapreduce.Partitioner;
public class MyPartitioner extends Partitioner<OutputKey, OutputValue> {
@Override
public int getPartition(OutputKey outputKey, OutputValue outputValue, int numPartitions) {
int result = (Integer.parseInt(outputKey.getMouth()) - 1) / 3;
return result;
}
}
这里说一下,我没有采用抽样,而是直接定义分区,每个reduce处理3个月的数据,一共四个reduce并行处理
- 最后创建驱动程序,先使用一个reduce处理
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
public class Sort {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
String[] path = {
"hdfs://localhost:9000/user/hduser/sort/input",
"hdfs://localhost:9000/user/hduser/sort/output"
};
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(Sort.class);
job.setOutputKeyClass(OutputKey.class);
job.setOutputValueClass(OutputValue.class);
job.setMapperClass(MyMapper.class);
job.setReducerClass(MyReducer.class);
job.setInputFormatClass(TextInputFormat.class);
job.setOutputFormatClass(TextOutputFormat.class);
// 设置四个reduce同步执行
// job.setNumReduceTasks(4);
// 设置Partitioner
// job.setPartitionerClass(MyPartitioner.class);
FileInputFormat.setInputPaths(job, new Path(path[0]));
FileSystem fs = FileSystem.get(conf);
Path p = new Path(path[1]);
if(fs.exists(p)) {
// 目录存在,删除
fs.delete(p, true);
}
FileOutputFormat.setOutputPath(job, p);
boolean a = job.waitForCompletion(true);
if(a) {
System.exit(0);
}
else {
System.exit(1);
}
}
}
这里需要说一下,因为之前都是每次运行完想要再次运行就需要自己手动去删除output文件夹,所以这次就直接在程序里,对HDFS的文件夹进行了操作,如果每次运行output文件夹都在,就会自动删除
-
查看结果
可以看到只使用一个reduce,只会产生一个结果文件,并且所有的数据都在文件中,文件根据月份升序,同时星期降序排序 -
接下来我们使用四个reduce进行并行处理,将
这两行注释放开,然后运行
- 查看结果
可以看到,首先产生了四个输出文件,而且按照我定义的分区规则,每三个月的数据在一个文件中,在每个文件中又都是符合排序规则的,四个文件接起来也是符合全排序的
写在最后的话
因为我是自己定义的分区,因为我们的数据量不大,又是伪分布式,所以如果想尝试数据抽样,然后根据数据分布自动创建分区的话,推荐个博客,自己看吧,我就不弄了
https://blog.csdn.net/u011596455/article/details/78069824