MapReduce编程练习1.0
1.0 初始需求
需求: 有一批数据,手机通过基站上网留下了一下访问数据,数据内容如下,需要统计每一个用户(手机号)所耗费的总上行流量、总下行流量,总流量。
问题点:
- 按从左至右的顺序,字段的含义:时间戳,手机号,基站地址(表示手机访问的是哪个基站),请求网站的IP,请求网站的域名,请求网站的简介,上行流量个数,下行流量个数,上行总流量,下行总流量,请求的状态码。
- 数据不是以空格切分,而是以 \t 来进行切分。
- 数据存在缺失情况,比如第二行缺失请求网站的域名和请求网站的简介。
- 最后输出的接口不是一个值,而是三个(总上行流量、下行流量,总流量)。
解析:
- 数据切分方式不用管,这个之前练习时有方法设置,
- 数据缺失这个比较难整,初看,手机号好获取,没有缺失情况,对每一行数据切分后取第二个值就是;总上行流量、总下行流量,总流量这三个顺着看位置有变动,没法确认,但是反向思路,我们从后面数,总上行流量、总下行流量,总流量这三个固定在后面倒数2、3、4位置,这个可以利用一下啊,对数据切分后,取倒数2、3、4位置的即可(使用length - n)。
- 输出值的问题,这个我们不可能跑三个程序来处理这批数据,如果后续有其他的需求,不就没得玩了吗?在Hadoop序列化机制中,我们可以定义自己的一个类来封装数据,但是这个类需要实现Hadoop的序列化接口Writable。所以这里我们可以定义一个实现Writable接口的类来解决这个问题,这个类中包含三个参数,就是我们需要获取的总上行流量、下行流量,总流量。
1.1 初始需求解决编程
1. 先编写我们自定义的数据封装类:FlowBean
package com.chinasofti.mapreducepractice.flowsum;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/*
* 自定义的数据封装类,用于封装需求数据
* Java 的序列化(Serializable)是一个重量级序列化框架,
* 一个对象被序列化后,会附带很多额外的信息(各种校验信息,header,继承体系…),不便于在网络中高效传输;
* hadoop 自己开发了一套序列化机制(**Writable**),精简,高效。
* 不用像 java 对象类一样传输多层的父子关系,需要哪个属性就传输哪个属性值,大大的减少网络传输的开销。
* 数据封装类需要实现序列化接口,Hadoop有自己的序列化接口Writable,实现Hadoop的Writable接口
*/
public class FlowBean implements Writable {
// 我们要保存三个数据,所以在类中定义三个参数
private long upflow = 0; // 上行流量
private long downflow = 0; // 下行流量
private long sumflow = 0; // 总流量
// 创建无参构造
// 注意:如果没有创建这个无参构造方法,再反序列化的时候会出错
public FlowBean(){
}
// 创建有参构造
public FlowBean(long upflow, long downflow, long sumflow) {
this.upflow = upflow;
this.downflow = downflow;
this.sumflow = sumflow;
}
//因为入参的时候有时会入下行流量和上行流量,没有入总流量,所有需要再建立一个这样的有参构造方法
public FlowBean(long upflow, long downflow) {
this.upflow = upflow;
this.downflow = downflow;
this.sumflow = upflow+downflow;
}
// bean对象常规操作:为属性提供get和set方法
public long getUpflow() {
return upflow;
}
public void setUpflow(long upflow) {
this.upflow = upflow;
}
public long getDownflow() {
return downflow;
}
public void setDownflow(long downflow) {
this.downflow = downflow;
}
public long getSumflow() {
return sumflow;
}
public void setSumflow(long sumflow) {
this.sumflow = sumflow;
}
// set数据时没有set方法,所以我们在这里手动编写一个set方法
// 主要实现:给属性复赋值
public void set(long upflow, long downflow){
this.upflow = upflow;
this.downflow = downflow;
this.sumflow = upflow+downflow;
}
//这就序列化方法
@Override
public void write(DataOutput dataOutput) throws IOException {
// 先序列化上行流量,再序列化下行流量,最后序列化总流量
// 序列化的方法在 DataOutput 类中以write开头的方法
dataOutput.writeLong(this.upflow);
dataOutput.writeLong(this.downflow);
dataOutput.writeLong(this.sumflow);
}
//这是反序列化方法
//反序列时候 注意序列化的顺序
//先序列化的先出来
@Override
public void readFields(DataInput dataInput) throws IOException {
// 注意反序列化的顺序,先反序列化上行流量,再反序列化下行流量,最后反序列化总流量
// 反序列化的方法在 DataInput 类中以read开头的方法
this.upflow = dataInput.readLong();
this.downflow = dataInput.readLong();
this.sumflow = dataInput.readLong();
}
// 重写toSting方法
@Override
public String toString() {
return upflow+"/t"+downflow+"/t"+sumflow;
}
}
2. 编写Mapper继承类:FlowSumMapper
这里需要注意,避免每一次调用map方法时都会去创建一次kv对象,所以我们需要提前创建好kv对象,因为v是我们自己创建Bean对象,没有set方法,所以我们需要再为其创建一个set方法。
package com.chinasofti.mapreducepractice.flowsum;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/*
* 这里就是mapreduce程序 mapper阶段业务逻辑实现的类
* KEYIN:KEYIN就表示每一行的起始偏移量 因此数据类型是Long
* VALUEIN:VALUEIN就表示读取的这一行内容 因此数据类型是String
* KEYOUT:KEYOUT表示手机号,因此数据类型是String
* VALUEOUT:VALUEOUT表示输出的封装好的数据,因此数据类型是 封装好的bean对象
*/
public class FlowSumMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
// mapper类输出的数据是两个对象,不能每运行一次map方法就创建两个对象,这势必会造成很多垃圾
// 我们可以先创建这两个对象,每次运行map方法时将值set这两个对象,从而避免创建多个对象的问题
Text k = new Text();
FlowBean v = new FlowBean();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 获取每一行的内容
String line = value.toString();
//将数据按 "/t" 进行切分,这个切分方式是根据数据源来决定的,
// 一句mmp不知当讲不当讲,\t写成了/t,导致没数据
String[] fields = line.split("\t");
//切分后即可获取手机号和上下行流量
// 手机号就是第二个
String phoneNum = fields[1];
// 上行流量是倒数第三个,切分完后数据类型是String,需要转换成Long
long upflow = Long.parseLong(fields[fields.length - 3]);
// 下行流量是倒数第二个,切分完后数据类型是String,需要转换成Long
long downflow = Long.parseLong(fields[fields.length - 2]);
// 为了避免重复创建对象而产生垃圾,我们先创建好key,value对象,然后通过各自的set方法去赋值
k.set(phoneNum);
// 因为数据封装类中没有set方法,所以我们自己去类中编写一个set方法
v.set(upflow,downflow);
// 通过context对象将数据传递给reduce
context.write(k,v);
}
}
3. 编写reducer继承类:FlowSumReducer
package com.chinasofti.mapreducepractice.flowsum;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* 这里是MR程序 reducer阶段处理的类
* KEYIN:就是reducer阶段输入的数据key类型,对应mapper的输出key类型 手机号
* VALUEIN就是reducer阶段输入的数据value类型,对应mapper的输出value类型 自己封装的bean对象
* KEYOUT就是reducer阶段输出的数据key类型 在本案例中 手机号
* VALUEOUTreducer阶段输出的数据value类型 在本案例中 求和后的总流量
*/
public class FlowSumReducer extends Reducer<Text, FlowBean, Text, FlowBean> {
FlowBean v = new FlowBean();
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException {
// 为了统计求和,获取手机流量的总和,这里设置两个参数用来保存值
long sumUpFlow = 0;
long sumDownFlow = 0;
// reduce做的事情很简单,只需要把流量拿出来统计即可
for (FlowBean flowBean:values) {
sumUpFlow += flowBean.getUpflow();
sumDownFlow += flowBean.getDownflow();
}
// key还是之前的手机号,所以不用变,
// value是计算后的值,这里同样为了避免创建多个对象而造成垃圾,先创建好对象后再来set值
v.set(sumUpFlow,sumDownFlow);
context.write(key,v);
}
}
4. 编写driver类:FlowSumDriver
package com.chinasofti.mapreducepractice.flowsum;
import org.apache.hadoop.conf.Configuration;
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 FlowSumDriver {
public static void main(String[] args) throws Exception {
//通过Job来封装本次mr的相关信息
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//指定本次mr job jar包运行主类
job.setJarByClass(FlowSumDriver.class);
//指定本次mr 所用的mapper reducer类分别是什么
job.setMapperClass(FlowSumMapper.class);
job.setReducerClass(FlowSumReducer.class);
//指定本次mr mapper阶段的输出 k v类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
//指定本次mr 最终输出的 k v类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
// 暂不用设置reduce数量
// job.setNumReduceTasks(3);
//如果业务有需求,就可以设置combiner组件
// job.setCombinerClass(FlowSumReducer.class);
/*
* 本地运行语句
* 注意本地运行时输出目录也不能存在,否则也会报错
*/
FileInputFormat.setInputPaths(job,"D:\\Practice_File\\hadoop_practice\\MapReduce\\flowsum\\input");
FileOutputFormat.setOutputPath(job,new Path("D:\\Practice_File\\hadoop_practice\\MapReduce\\flowsum\\output"));
// job.submit();
//提交程序 并且监控打印程序执行情况
boolean b = job.waitForCompletion(true);
System.exit(b?0:1);
}
}