目录
第一章 MapReduce
之前介绍了Hadoop中的HDFS,现在对Hadoop中的MapReduce进行介绍,MapReduce是一个分布式计算编程框架,之所以称之为框架,是因为它帮助我们封装了许多底层的实现,比如,读取文件等。这些操作都不需要我们实现,我们只需要将注意力放在处理业务,编写业务代码即可。
我们以最常用的单词统计为例,体验一下MapReduce的方便。单词统计就是统计一篇文章中,各个单词出现的次数。我分别通过两种方式实现,一是自己写Java代码,二是使用MapReduce
测试用例:
hello name tom
hello rose hello hadoop
hello jack rose hadoop
1.1 用Java写一个WordCount(单词统计)程序
1.1.1 统计一个文件中,每个单词出现的次数
编程思路:
1)创建输入流,对文件进行按行读取
2)创建容器,存放读取的单词;该容器应该大小可变,推荐使用:Map<单词,次数>
3)按行读取数据时,将每行中的每个单词放入Map中。如果不存在,则以该单词为Key,以1为value,添加到Map中;如果存在,将value值取出,加一后更新该word的value;
代码实现:
package com.ethan.mr;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.HashMap;
import java.util.Map;
public class WordCount01 {
public static void main(String[] args) {
try {
// 创建一个流:对文件进行读取
// 创建一个容器:存放读取的单词;容器大小可变,Map<单词,次数>
BufferedReader br = new BufferedReader(new FileReader("F:\\mrtest\\word.txt"));
Map<String, Integer> map = new HashMap<String, Integer>();
// 文件进行读取,并向map中添加
String line = null;
while((line = br.readLine()) != null) {
// 将读取到的一行内容进行拆分
String[] split = line.split("\t");
// 将数据中的每个单词放入Map中
for (String word : split) {
// 如果不存在,则以wword为key,1为value,添加到map中
if(!map.containsKey(word)) {
map.put(word, 1);
}else {
// 如果存在,将value值取出 + 1
int value = map.get(word) + 1;
map.put(word, value);
}
}
}
System.out.println(map);
} catch (Exception e) {
e.printStackTrace();
}
}
}
1.1.2 统计多个文件中,每个单词出现的总次数
编程思路:分而治之
1)每一次统计一个文件的单词出现的次数,与1.1.1中一样
2)最后将第一步中的结果进行合并
代码实现:
package com.ethan.mr;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
public class WordCount02 {
public static void main(String[] args) {
// 1.分别统计
Map<String, Integer> map01 = readOneFile("F:\\mrtest\\word01.txt");
Map<String, Integer> map02 =readOneFile("F:\\mrtest\\word02.txt");
Map<String, Integer> map03 =readOneFile("F:\\mrtest\\word03.txt");
Map<String, Integer> map04 =readOneFile("F:\\mrtest\\word04.txt");
Map<String, Integer> map05 =readOneFile("F:\\mrtest\\word05.txt");
Map<String, Integer> map06 =readOneFile("F:\\mrtest\\word06.txt");
System.out.println(map01);
System.out.println(map02);
System.out.println(map03);
System.out.println(map04);
System.out.println(map05);
System.out.println(map06);
// 2.合并
Map<String,Integer>mergerResult=mergerResult(map01,map02,map03,map04,map05,map06);
System.out.println("最终统计:" + mergerResult);
}
/**
* 统计单个文件中的单词个数
* @param filePath
* @return
*/
public static Map<String, Integer>readOneFile(String filePath) {
Map<String, Integer> map = new HashMap<String, Integer>();
try {
// 创建一个流:对文件进行读取
// 创建一个容器:存放读取的单词;容器大小可变,Map<单词,次数>
BufferedReader br = new BufferedReader(new FileReader(filePath));
// 文件进行读取,并向map中添加
String line = null;
while((line = br.readLine()) != null) {
// 将读取到的一行内容进行拆分
String[] split = line.split("\t");
// 将数据中的每个单词放入Map中
for (String word : split) {
// 如果不存在,则以wword为key,1为value,添加到map中
if(!map.containsKey(word)) {
map.put(word, 1);
}else {
// 如果存在,将value值取出 + 1
int value = map.get(word) + 1;
map.put(word, value);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
/**
* 文件合并
* 循环遍历每一个map集合,将最终结果放在一个新的map中
* @param map
* @return
*/
public static Map<String,Integer> mergerResult(Map<String, Integer> ... maps) {
Map<String,Integer> resultMap = new HashMap<String, Integer>();
// 对可变参数进行循环遍历,获取到每一个map
for (Map<String, Integer> m : maps) {
// 循环遍历每一个Map,将结果放到结果Map中
for(Entry<String, Integer> kv : m.entrySet()) {
// 取出Entry中的每一个key进行判断
// 如果不存在,存到新的Map中
String key = kv.getKey();
int value = kv.getValue();
if(!resultMap.containsKey(key)) {
resultMap.put(key, value);
}else {// 如果存在 hello-8 hello-4 原始的value取出来 + 新的value值
Integer valueNew = resultMap.get(key) + value;
resultMap.put(key, valueNew);
}
}
}
return resultMap;
}
}
根据以上例子发现,如果进行多个文件数据的统计,我们的实现思路和代码框架,都是相同的。所以可以将这种解决问题的框架提取出来,用户只需要根据自己的业务逻辑进行实现就可以。
在以上例子中,readOneFile、mergerResult分别简单模拟了MapReduce计算框架。readOneFile()方法可以看成map();mergerResult()方法可以看成reduce();我们可以初步的理解为Map:分部分统计;Reduce:结果合并。
1.2 用MapReduce框架编写WordCount
MapReduce中已经为我们封装好了Mapper类和Reducer类。如果我们有自己的业务逻辑,只需要继承相应的类,并重写其中的map和reduce方法即可。
1.2.1 序列化 & 反序列化
因为MR处理的文件分布在不同的节点中,处理数据时必然经过持久化磁盘和网络传输的过程,所以数据必须经过序列化和反序列化。
Java中的序列化接口是Serializable,是连同类进行序列化的,过于累赘。所以,Hadoop弃用Java中的序列化接口,而提供了一套轻便的Writable接口,只对值进行序列化。Hadoop中提供的数据类型是已经实现Writable接口的。
Java基本数据类型与Hadoop基本数据类型的对应关系:(部分)
int | IntWritable |
long | LongWritable |
double | DoubleWritable |
Float | FloatWritable |
String | Text |
Null | NullWritable |
Java中的数据类型转化为Hadoop中的数据类型,只需要new Hadoop数据类型(Java中的值)即可。
Hadoop类型转为Java类型用get()方法即可。
String javaString = "hello";
// 将Java中String类型转为Hadoop中对应的Text类型
Text hadoopText = new Text(javaString);
// 将Hadoop中Text类型转为Java中对应的String类型
String javaExchange = hadoopText.get();
1.2.2 继承Mapper类(Map端)
public class WordCountMapper extends Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {}
1)介绍Mapper类中的四个泛型
Mapper的输入,就是需要统计的文件;这个文件是以一行一行的方式提供给使用者。(底层调用类似9.1.1中的br.line)。
① KEYIN:Map端输入的Key的泛型,这里指的是每一行的起始偏移量。
MR底层实现文件输入依赖字节流。当读取到\r\n时,代表读取一行结束。
② VALUEIN:Map端输入的值的类型,这里指的是一行的内容。
③ KEYOUT:Map端输出的Key的类型,这里是指的是单词。
④ VALUEOUT:Map端输出的value类型,这里指的是标记1,便于统计。
2)继承Mapper类后,重写其map方法
map方法的调用频率:每读取完一行调用一次
方法参数:
LongWritable key:每一行的起始偏移量
Text value:每一行的内容(每次读取的那一行内容)
Context context:内容对象(上下文对象),用于传输的对象,发送给Reduce方法
package com.ethan.mr;
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
/**
* 分部统计
* @author Ethan
*
*/
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
/**
* 方法调用频率:一行调用一次
* LongWritable key:每一行的起始偏移量
* Text value:每一行的内容(每次读取的那一行内容)
* Context context:内容对象(上下文对象),用于传输的对象,发送给Reduce
*/
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
// 获取到每一行的内容,将每个单词加标签
String line = value.toString();
// 进行每个单词的切分
String[] words = line.split("\t");
// 循环遍历打标记
for(String w : words) {
context.write(new Text(w), new IntWritable(1));
}
}
}
1.2.3 继承Reducer类(Reduce端)
Reducer类处理的是Mapper端输出的结果,即reduce方法的输入,是map方法的输出。
public class WordCountReducer extends Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT>{}
1)Reducer类中的四个泛型
① KEYIN:Reducer端输入的key的类型,是Mapper端输出的key的类型;本例是Text类型。
② VALUEIN:Reducer端输入的value的类型,是Mapper端输出的value的类型;本例是IntWritable类型。
③ KEYOUT:Reducer统计结果的key的类型,这里指的最终统计完成的单词(Text类型)
④ VALUEOUT:Reducer统计结果的value的类型,这里是指单词出现的总次数(IntWritable类型)
2)继承Reducer类,重写reduce方法
reduce方法的调用频率:每一组调用一次,key相同的为一组
Mapper端的输出结果由Context.write()方法发送至Reducer端,本例中的数据格式应为:<hello,1>、<word,1>、<hello,1>...<Java,1>等。
Mapper端输出的数据到达Reducer端之前,MR框架内部帮助我们对数据进行了整理,叫分组。默认的分组是按照map输出的key进行分组,即key相同的为一组。Mapper端有多少不同的key,就有多少组。分组的详细介绍:
方法参数:
Text key:每一组中相同的key
Iterable<IntWritable> values:每一组中相同的key对应的所有value值;这里代表hello <1,1,…,1,1>
Context context:内容对象(上下文对象);可将数据写出到HDFS
package com.ethan.mr;
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
/**
* 合并单词出现的次数
* @author Ethan
*
*/
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable>{
/**
* Text key:每一组中的相同的那个key
* Iterable<IntWritable> values:每一组中的所有value值
* Context context:内容对象(上下文对象),用于传输,写出到HDFS
*/
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context)
throws IOException, InterruptedException {
int count = 0; // 计数
// 统计结果,循环遍历values
for (IntWritable i : values) {
int value = i.get(); // hadoop数据类型转为java数据类型
count = value + count;
}
context.write(key, new IntWritable(count));
}
}
1.2.4 驱动类(代码提交类)
在MapReduce中,一个计算程序叫做一个Job,用于封装计算程序中的Mapper和Reducer,以及输入和输出。
驱动类的编写步骤:
①加载配置文件
②创建一个Job,并在Job中分别设置:主驱动类、Mapper和Reducer类、设置Mapper的输出类型、设置Reducer的输出类型、设置输入路径和输出路径等信息。
输入路径:HDFS中需要统计单词的文件的路径
输出路径:最终统计结果输出的路径;注意:输出路径一定不能存在!
③Job提交
代码实现:
package com.ethan.mr;
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
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;
/**
* 驱动类:代码提交类
* @author Ethan
*/
public class Driver {
public static void main(String[] args) {
try {
// 加载配置文件
Configuration conf = new Configuration();
// 启动一个Job
Job job = Job.getInstance(conf);
// 设置程序的主驱动类,运行的时候打成Jar包运行
job.setJarByClass(Driver.class);
// 设置Mapper和Reducer类
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
// 设置Mapper的输出类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 设置Reducer的输出类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 设置输入,输出路径 (addInputPath和setInputPath都行)
// args[0]:代表运行代码时,控制台手动输入的参数
// 输入路径:需要统计单词的路径
FileInputFormat.addInputPath(job, new Path(args[0]));
// 输出路径:最终统计结果输出的路径
// 注意:输出路径一定不能存在
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// job提交,true:执行打印日志
job.waitForCompletion(true);
} catch (ClassNotFoundException | InterruptedException | IOException e) {
e.printStackTrace();
}
}
}
最后将代码打成Jar包,在集群中运行;运行指令:Hadoop jar xxx.jar driver所在包 输入路径 输出路径
1.3 总结MapReduce的编程套路
在MR程序中分为map阶段(WordCountMapper)和reduce阶段(WordCountReducer)。
①map阶段:主要进行取出数据,进行切分,打标签,发送给reduce
②map至reduce见的中间过程:分组
③reduce阶段:对map的输出结果进行合并