(1)数据样例
13726238888 2481 24681
13560436666 1116 954
13726230503 2481 24681
13826544101 264 0
13926435656 132 1512
13926251106 240 0
18211575961 1527 2106
(2)字段释义
字段中文释义 | 字段英文释义 | 数据类型 |
---|---|---|
手机号 | phone | String |
上行流量 | upflow | Long |
下行流量 | downflow | Long |
(3)项目需求一
统计每一个用户(手机号)所耗费的总上行流量、总下行流量、总流量。
期望输出数据格式:
13480253104 2494800 2494800 4989600
(4)项目解析
其实此需求和之前我们写的WordCount有点类似,也是按照key进行求和,只不过此时的key不是每个单词,而是手机号,此时的value不是出现的次数1,而是手机号对应的**{上行流量,下行流量,上行流量+下行流量}**。
根据之前的 WordCount 示例我们可以联想到,我们需要将**<手机号,{上行流量,下行流量,上行流量+下行流量}>**作为 Map 端的输出,传递给 Reduce 端,让Reduce端按照key,即手机号做汇总操作。
所以此时的 Map 端的输出value不再是基础类型了,所以我们需要自定义 bean 类来封装流量信息,把 {上行流量,下行流量,上行流量+下行流量} 封装成一个对象。
序列化输出类 FlowBean 上一节课我们已经定义完成,本节课我们主要实现的是,利用 MapReduce 完成按照手机号 对 value 进行汇总。
回顾 MapReduce Map 端编码规范:
1. 用户自定义的 Mapper 需要继承父类 Mapper
2. Mapper 的输入数据是 KV 对的形式(KV 的类型可自定义)
3. Mapper 的输出数据是 KV 对的形式(KV 的类型可自定义)
4. Mapper 中的业务逻辑写在 map() 方法中
5. MapTask 进程对每一个
调用一次map()方法
接下来进入 Map 端程序的编写,eclipse 成功连接到 Hadoop 集群后,选择“File”->“New”->“Project”->“Map/Reduce Project”创建名为 MyMR 的项目名,在此项目下创建名为 com.hongyaa.sum
的包名,在此包下创建名为 FlowSumMapper.java 的类,如下图所示:
图1
首先编写 Map 端编程框架,自定义的 FlowSumMapper 需要继承父类 Mapper,输入数据和输出数据都是KV 对的形式。具体框架代码如下:
public class FlowSumMapper extends Mapper{
}
已知 Mapper 中的业务逻辑写在 map() 方法中,在此 map()方法中我们需要实现的是将读取到的每一行文本拆分成3列,分别是手机号,上行流量和下行流量,对**{上行流量,下行流量}**进行封装,每遇到一个手机号就把其转换成一个 key-value 对,比如手机号 13726238888,就转换成<13726238888,{2481,24681,27162}>
发送给 ReduceTask 去汇总。具体代码如下所示:
@Override
protected void map(LongWritable key, Text value, Mapper.Context context)
throws IOException, InterruptedException {
//(1)将maptask传给我们的每行文本内容先转换成String
String line = value.toString();
//(2)根据空格将这一行切分成单词
String[] splits = line.split("\t");
//(3)抽取业务所需的字段
String telephone = splits[0]; // 手机号
String upFlow = splits[1]; // 上行流量
String downFlow = splits[2]; // 下行流量
//(4)获取上行流量和下行流量,对其进行封装
FlowBean fb = new FlowBean(Long.parseLong(upFlow), Long.parseLong(downFlow));
//(5)将手机号作为key,将封装的流量信息类作为value
context.write(new Text(telephone), fb);
}
FlowSumMapper.java 的完整代码如下所示:
public class FlowSumMapper extends Mapper {
@Override
protected void map(LongWritable key, Text value, Mapper.Context context)
throws IOException, InterruptedException {
//(1)将maptask传给我们的每行文本内容先转换成String
String line = value.toString();
//(2)根据空格将这一行切分成单词
String[] splits = line.split("\t");
//(3)抽取业务所需的字段
String telephone = splits[0]; // 手机号
String upFlow = splits[1]; // 上行流量
String downFlow = splits[2]; // 下行流量
//(4)获取上行流量和下行流量,对其进行封装
FlowBean fb = new FlowBean(Long.parseLong(upFlow), Long.parseLong(downFlow));
//(5)将手机号作为key,将封装的流量信息类作为value
context.write(new Text(telephone), fb);
}
}
回顾 MapReduce Reduce 端编码规范:
1. 用户自定义的 Reducer 需要继承父类 Reducer
2. Reducer 的输入数据类型对应 Mapper 的输出数据类型,也是 KV
3. Reducer 的输出数据是 KV 对的形式(KV 的类型可自定义)
4. Reducer 的业务逻辑写在 reduce() 方法中
5. ReduceTask 进程对每一组相同 k 的
组调用一次 reduce() 方法
接下来进入 Reduce 端程序的编写,在 com.hongyaa.sum
包下创建名为 FlowSumReducer.java 的类,如下图所示:
图2
首先编写 Reduce 端编程框架,自定义的 FlowSumReducer 需要继承父类 Reducer,输入数据和输出数据都是KV 对的形式。具体框架代码如下:
public class FlowSumReducer extends Reducer {
}
已知 Reducer 中的业务逻辑写在 reduce() 方法中,在此 reduce()方法中我们需要接收 MapTask 的输出结果,然后按照 key(手机号) 对 value(上行流量、下行流量) 做汇总操作。具体代码如下所示:
@Override
protected void reduce(Text key, Iterable values,
Reducer.Context context) throws IOException, InterruptedException {
long sumUpFlow = 0L; // 总上行流量
long sumDownFlow = 0L; // 总下行流量
for (FlowBean fb : values) {
sumUpFlow += fb.getUpFlow();
sumDownFlow += fb.getDownFlow();
}
// 获取总上行流量和总下行流量,对其进行封装
FlowBean resultsum = new FlowBean(sumUpFlow, sumDownFlow);
// 将手机号作为key,将封装的流量信息类作为value,写出最终结果
context.write(key, resultsum);
}
FlowSumReducer.java 的完整代码如下所示:
public class FlowSumReducer extends Reducer {
/**
* 框架每传递一组数据<13502468823,{FlowBean,FlowBean,FlowBean...}>调用一次reduce方法
* reduce中的业务逻辑是遍历values,然后进行累加求和后输出
*/
@Override
protected void reduce(Text key, Iterable values,
Reducer.Context context) throws IOException, InterruptedException {
long sumUpFlow = 0L; // 总上行流量
long sumDownFlow = 0L; // 总下行流量
for (FlowBean fb : values) {
sumUpFlow += fb.getUpFlow();
sumDownFlow += fb.getDownFlow();
}
// 获取总上行流量和总下行流量,对其进行封装
FlowBean resultsum = new FlowBean(sumUpFlow, sumDownFlow);
// 将手机号作为key,将封装的流量信息类作为value,写出最终结果
context.write(key, resultsum);
}
}
回顾 MapReduce Driver 端编码规范:整个程序需要一个 Drvier 来进行提交,提交的是一个描述了各种必要信息的 job 对象。
接下来进入 Driver 端程序的编写,在 com.hongyaa.sum
包下创建名为 FlowSum.java 的类,如下图所示:
图3
Driver 端为该 FlowSum 程序运行的入口,相当于 YARN 集群(分配运算资源)的客户端,需要创建一个 Job 类对象来管理 MapReduce 程序运行时需要的相关运行参数,最后将该 Job 类对象提交给 YARN。
Job对象指定作业执行规范,我们可以用它来控制整个作业的运行。接下来,我们来看一下作业从提交到执行的整个过程。
FlowSum.java 的完整代码如下所示:
public class FlowSum {
/**
* 项目需求:统计每一个用户(手机号)所耗费的总上行流量、总下行流量、总流量;
*
*/
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// (1)创建配置文件对象,指定mapreduce程序所需的 HDFS 相关参数
Configuration conf = new Configuration();
// 本地运行模式
conf.set("fs.defaultFS", "hdfs://localhost:9000");
// (2)新建一个 job 任务
Job job = Job.getInstance(conf);
// (3)将 job 所用到的那些类(class)文件,打成jar包 (打成jar包在集群运行必须写)
job.setJarByClass(FlowSum.class);
// (4)指定Mapper和Reducer组件
job.setMapperClass(FlowSumMapper.class);
job.setReducerClass(FlowSumReducer.class);
// (5)指定MapTask和ReduceTask的输出key-value类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
// (6)指定该MapReduce程序数据的输入输出路径
Path inPath = new Path("/flow/input");
Path outPath = new Path("/flow/output_sum");
// 获取fs对象
FileSystem fs = FileSystem.get(conf);
// 判断输出路径是否存在
if (fs.exists(outPath)) {
// 输出路径存在则递归删除
fs.delete(outPath, true);
}
FileInputFormat.addInputPath(job, inPath);
FileOutputFormat.setOutputPath(job, outPath);
// (7)最后提交任务给YARN来运行,等着集群运行完成返回反馈信息,客户端退出
boolean waitForCompletion = job.waitForCompletion(true);
System.exit(waitForCompletion ? 0 : 1);
}
}
执行结果如下所示:
图4