七、MapReduce经典案例
(一)好友推荐案例
1、需求
推荐好友的好友,比如给hadoop推荐cat、hello、mr。
(需求实际就是获取非好友的两个人有多少共同好友)
2、数据准备
双向好友关系
tom:hello hadoop cat
world:hadoop hello hive
cat:tom hive
mr:hive hello
hive:cat hadoop world hello mr
Hadoop:tom hive world
Hello:tom world hive mr
3、思路
推荐者与被推荐者一定有一个或多个相同的好友,转变为找共同好友,但是,两人不能是直接好友,例如,针对第一行,可以给hello推荐hadoop,也可以给hadoop推荐hello,但是,两者不能为直接好友才可以,例如,hadoop跟world有共同好友hive,此时,是不能给他们互相推荐的,因为hadoop跟word已经是直接好友了。
全局去寻找好友列表中两两关系,这种两两关系体现出来的他们是间接好友,并且只要他们组建了,就证明他们是有公共好友的,若共同好友是tom,可以给它们互相推荐,例如,第一行中的hello:hadoop、hello:cat、hadoop:cat,但是如果他们是直接好友的话,就不能推荐了,例如第5行中hadoop与world体现出了是间接好友,他们有共同好友hive,可以给他们互相推荐,但是在第二行里面,world与hadoop体现出的是直接好友,因此就不能给他们互相推荐了。所以要从这里面剔除直接好友。那么,直接好友去哪里找呢?
全局去寻找好友依次与好友的两两关系,这种关系体现出来的就是直接好友。就是每行第一个与剩余的每个好友依次组建的两两关系。例如,tom:hello、 tom:hadoop、tom:cat
因此所有这些两两关系中,既有直接好友关系,也有间接好友关系;要从间接好友中,去除直接好友;
统计两两关系出现次数,即他们共同好友的个数。
代码思路:
map:按好友列表输出两俩关系
reduce:sum两两关系
结果:
cat:hadoop 2
cat:hello 2
cat:mr 1
cat:world 1
hadoop:hello 3
hadoop:mr 1
hive:tom 3
mr:tom 1
mr:world 2
tom:world 2
4、代码实现
(1)自定义RecommendMapper
package com.bigdata.recommendfriends;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class RecommendMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1 读取每行数据 tom:hello hadoop cat
String line = value.toString();
// 2 挑出人,好友列表
// tom
String person = line.split(":")[0];
// [hello,hadoop,cat]
String[] friends = line.split(":")[1].split(" ");
// 3 遍历好友列表 组装间接好友 <友:友,1> 组装直接好友<友:人,0>
for(int i =0 ;i <= friends.length-1;i++){
// hello
String friend = friends[i];
// 组装直接好友 <友:人,0> 直接好友用valueout的0表示
context.write(new Text(getFd(person,friend)),new IntWritable(0));
for(int j = i+1;j<=friends.length-1;j++){
// 组装间接好友 <友:友,1> 间接好友用valueout的1表示
// 有时候 cat:hadoop 有时候 hadoop:cat 但是业务需要 cat:hadoop
context.write(new Text(getFd(friend,friends[j])),new IntWritable(1));
}
}
// 4 将kv写出
}
public static String getFd(String a,String b){
return a.compareTo(b) < 0? a+":"+b:b+":"+a;
}
}
(2)自定义RecommendReducer
package com.bigdata.recommendfriends;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
// keyout 是要推荐的人
// valueout 是他们之间认识的共同好友的数量
public class RecommendReduce extends Reducer<Text, IntWritable,Text,IntWritable> {
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
// 同一个分组的数据有两大种可能性:一种是value含有0的,这种数据证明他们两人本来认识了,这种情况不能推荐
// <hadoop:world,0>
// <hadoop:world,1>
// 另一种是value全是1的,这种数据证明他们两人不认识,需要做推荐
// <hadoop:hello,1>
// <hadoop:hello,1>
// <hadoop:hello,1>
// 循环每个分组的value,只要遇到value是0的情况,就停下来
//定义共同好友的数量
int sum = 0;
for (IntWritable value : values) {
int i = value.get();
if(i == 0){
return;
}
sum = sum+i;
}
context.write(key,new IntWritable(sum));
}
}
(3)自定义RecommendDriver
package com.bigdata.recommendfriends;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
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;
public class RecommendDriver {
public static void main(String[] args) throws Exception {
// 1 创建一个配置对象
Configuration conf = new Configuration();
// 2 通过配置对象创建一个job
Job job = Job.getInstance(conf);
// 3 设置job的mr的路径(jar包的位置)
job.setJarByClass(RecommendDriver.class);
// 4 设置job的mapper类 reduce类
job.setMapperClass(RecommendMapper.class);
job.setReducerClass(RecommendReduce.class);
// 5 设置job的mapper类的keyout,valueout
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 6 设置job的最终输出的keyout,valueout
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 7 设置job的输入数据的路径
FileInputFormat.setInputPaths(job,new Path(args[0]));
// 8 设置job的输出数据的路径 得保证,输出目录不能事先存在,否则报错,
Path outPath = new Path(args[1]);
FileSystem fs = FileSystem.get(conf);
if(fs.exists(outPath)){
fs.delete(outPath,true);
}
FileOutputFormat.setOutputPath(job,outPath);
// 9 提交job到yarn集群
boolean b = job.waitForCompletion(true);
System.out.println("是否运行成功:"+b);
}
}
(二)数据清洗案例
数据清洗:是指发现并纠正数据文件中可识别的错误的最后一道程序,包括检查数据一致性,处理无效值和缺失值等。与问卷审核不同,录入后的数据清理一般是由计算机而不是人工完成。
ETL:是英文Extract-Transform-Load的缩写,用来描述将数据从来源端经过抽取(extract)、转换(transform)、加载(load)至目的端的过程。ETL一词较常用在数据仓库,但其对象并不限于数据仓库。
1、需求
将日志按照空格分隔,去除每条日志中字段组成数组的长度小于等于11的日志
2、代码实现
(1)编写LogMapper
package com.bigdata.etl;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class LogMapper extends Mapper<LongWritable,Text,Text,NullWritable>{
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1 读取每行数据 194.237.142.21 - - [18/Sep/2013:06:49:18 +0000] "GET /wp-content/uploads/2013/07/rstudio-git3.png HTTP/1.1" 304 0 "-" "Mozilla/4.0 (compatible;)"
String line = value.toString();
// 2 按照空格切分,查看长度,解析日志
boolean flag = parseLog(line,context);
if(!flag){
return;
}
// 3 将合法的数据写出
context.write(value,NullWritable.get());
}
private boolean parseLog(String line,Context context) {
String[] split = line.split(" ");
if(split.length >11){
context.getCounter("logGroup","trueLogCounter").increment(1);
return true;
}else{
context.getCounter("logGroup","falseLogCounter").increment(1);
return false;
}
}
}
(2)编写LogDriver
package com.bigdata.etl;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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 LogDriver {
public static void main(String[] args) throws Exception {
// 1 创建一个配置对象
Configuration conf = new Configuration();
// 2 通过配置对象创建一个job
Job job = Job.getInstance(conf);
// 3 设置job的mr的路径(jar包的位置)
job.setJarByClass(LogDriver.class);
// 4 设置job的mapper类 reduce类
job.setMapperClass(LogMapper.class);
// 5 设置job的mapper类的keyout,valueout
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
// 6 设置job的最终输出的keyout,valueout
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// 7 设置job的输入数据的路径
FileInputFormat.setInputPaths(job,new Path(args[0]));
// 8 设置job的输出数据的路径 得保证,输出目录不能事先存在,否则报错,
Path outPath = new Path(args[1]);
FileSystem fs = FileSystem.get(conf);
if(fs.exists(outPath)){
fs.delete(outPath,true);
}
FileOutputFormat.setOutputPath(job,outPath);
// 因为数据清洗,不涉及数据的累加,因此就不需要reduce,这里设置reduce的数量为0
job.setNumReduceTasks(0);
// 9 提交job到yarn集群
boolean b = job.waitForCompletion(true);
System.out.println("是否运行成功:"+b);
}
}
(三)气温指数分析
1、数据准备
1949-10-01 14:21:02 34c
1949-10-03 14:01:02 37c
1949-10-05 14:01:02 35c
1949-10-04 14:01:02 36c
1949-10-02 14:01:02 38c
1950-01-01 11:21:02 32c
1950-10-01 12:21:02 37c
1951-12-01 12:21:02 23c
1950-10-02 12:21:02 41c
1950-10-03 12:21:02 27c
1951-07-01 12:21:02 45c
1951-07-02 12:21:02 46c
1951-07-03 12:21:03 47c
2、需求
找出每个月气温最高的2天
结果:
1949 10 2 38
1949 10 3 37
1950 1 1 32
1950 10 2 41
1950 10 1 37
1951 7 3 47
1951 7 2 46
1951 12 1 23
3、思路
每年->每个月->最高->2天
1天多条记录情况如何解决?
进一步思考:
- 年升序月升序温度降序(温度高的优先被挑出来),
- 年月分组
实现步骤:
通过GroupCompartor设置分组规则,保证相同年月的分到同个组并遍历所有value,找出温度最高值,并记录对应日期再找温度次高值,并且确保该次高值的天与温度最高值所对应的日期不一样
mapreduce是按key排序,所以key中要包含时间和温度,自定义数据类型TianQi(包含时间、温度),自定义排序比较规则
自定义分组比较,按照相同的年月分组即可
4、代码实现
(1)自定义序列化类及排序WeatherBean
package com.bigdata.tianqi;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class Tianqi implements WritableComparable<Tianqi>{
private int year;
private int month;
private int day;
private int wd;
public Tianqi() {
}
public Tianqi(int year, int month, int day, int wd) {
this.year = year;
this.month = month;
this.day = day;
this.wd = wd;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public int getMonth() {
return month;
}
public void setMonth(int month) {
this.month = month;
}
public int getDay() {
return day;
}
public void setDay(int day) {
this.day = day;
}
public int getWd() {
return wd;
}
public void setWd(int wd) {
this.wd = wd;
}
// 按照year升序,month升序,wd降序
public int compareTo(Tianqi o) {
int res1 = Integer.compare(this.getYear(),o.getYear());
if(res1 == 0){
int res2 = Integer.compare(this.getMonth(),o.getMonth());
if(res2 == 0){
return -Integer.compare(this.getWd(),o.getWd());
}
return res2;
}
return res1;
}
public void write(DataOutput out) throws IOException {
out.writeInt(year);
out.writeInt(month);
out.writeInt(day);
out.writeInt(wd);
}
public void readFields(DataInput in) throws IOException {
this.year = in.readInt();
this.month = in.readInt();
this.day = in.readInt();
this.wd = in.readInt();
}
@Override
public String toString() {
return this.year+"\t"+this.month+"\t"+this.day+"\t"+this.wd+"c";
}
}
(2)自定义WeatherMapper
package com.bigdata.tianqi;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
public class TianqiMapper extends Mapper<LongWritable, Text, Tianqi, NullWritable> {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Tianqi k = new Tianqi();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1 读取每行数据,1949-10-01 14:21:02 34c
String line = value.toString();
// 2 挑出年,月,天,温度 设置进bean,
String[] split = line.split("\t");
String timeStr = split[0];
try {
// 1949-10-01
Date date = sdf.parse(timeStr);
Calendar calendar = Calendar.getInstance();
// 1949-10-01
calendar.setTime(date);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1;
int day = calendar.get(Calendar.DAY_OF_MONTH);
k.setYear(year);
k.setMonth(month);
k.setDay(day);
// 34c
String wdStr = split[1];
String wd = wdStr.replace("c", "");
k.setWd(Integer.parseInt(wd));
} catch (ParseException e) {
e.printStackTrace();
}
// 3 组装kv并写出
context.write(k,NullWritable.get());
}
}
(3)自定义TianQiReucer
package com.bigdata.tianqi;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class TianqiReduce extends Reducer<Tianqi, NullWritable,Tianqi, NullWritable>{
@Override
protected void reduce(Tianqi key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
//<1949-10-01 38,null>
//<1949-10-01 37,null>
//<1949-10-03 36,null>
//<1949-10-05 36,null>
//<1949-10-04 36,null>
//<1949-10-02 36,null>
//<1949-10-01 34,null>
// 用于标记是否将最高温度写出
int flag = 0;
// 用于标记最高温度是哪天
int day = 0;
for (NullWritable value : values) {
// 先找出温度最高的那天
if(flag == 0){
context.write(key,NullWritable.get());
day = key.getDay();
flag ++;
}
// 写出次高温度
if(day != key.getDay()){
context.write(key,NullWritable.get());
break;
}
}
}
}
(4)自定义分组WeatherGroup
package com.bigdata.tianqi;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;
public class TianqiGroupComparator extends WritableComparator{
public TianqiGroupComparator() {
super(Tianqi.class,true);
}
@Override // 给出一个标准,只要两个bean的year,month相同,则认为是这两个bean相同,则认为是一组,调用一次reduce方法
public int compare(WritableComparable a, WritableComparable b) {
Tianqi aa = (Tianqi) a;
Tianqi bb = (Tianqi) b;
int res1 = Integer.compare(aa.getYear(),bb.getYear());
if(res1 == 0){
return Integer.compare(aa.getMonth(),bb.getMonth());
}
return res1;
}
}
(5)自定义WeatherDriver
package com.bigdata.tianqi;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class TianqiDriver {
public static void main(String[] args) throws Exception {
// 1 创建一个配置对象
Configuration conf = new Configuration();
// 2 通过配置对象创建一个job
Job job = Job.getInstance(conf);
// 3 设置job的mr的路径(jar包的位置)
job.setJarByClass(TianqiDriver.class);
// 4 设置job的mapper类 reduce类
job.setMapperClass(TianqiMapper.class);
job.setReducerClass(TianqiReduce.class);
// 5 设置job的mapper类的keyout,valueout
job.setMapOutputKeyClass(Tianqi.class);
job.setMapOutputValueClass(NullWritable.class);
// 6 设置job的最终输出的keyout,valueout
job.setOutputKeyClass(Tianqi.class);
job.setOutputValueClass(NullWritable.class);
// 7 设置job的输入数据的路径
FileInputFormat.setInputPaths(job,new Path(args[0]));
// 8 设置job的输出数据的路径 得保证,输出目录不能事先存在,否则报错,
Path outPath = new Path(args[1]);
FileSystem fs = FileSystem.get(conf);
if(fs.exists(outPath)){
fs.delete(outPath,true);
}
FileOutputFormat.setOutputPath(job,outPath);
job.setGroupingComparatorClass(TianqiGroupComparator.class);
// 在写分区器的时候,必须保证同年同月的数据在一个区
job.setPartitionerClass(TianqiPartitioner.class);
job.setNumReduceTasks(3);
// 9 提交job到yarn集群
boolean b = job.waitForCompletion(true);
System.out.println("是否运行成功:"+b);
}
}
分区器:
package com.bigdata.tianqi;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Partitioner;
public class TianqiPartitioner extends Partitioner<Tianqi, NullWritable> {
@Override
public int getPartition(Tianqi tianqi, NullWritable nullWritable, int numPartitions) {
return (tianqi.getYear() & Integer.MAX_VALUE) % numPartitions;
}
}
八、MapReduce Join关联
所谓的map reduce join是指采用mapreduce程序把多个输入文件关联到一个文件中去并输出,类似于在mysql中,多个表之间的关联。
Map reduce join按照实现不同,分为两类,一类是在reduce端实现的关联,称之为reduce join,另一类是在map端实现的关联,称之为map join。
1、Reduce join(合并)
原理
Map端的主要工作:为来自不同表(文件)的key/value键值对打标签以区别不同来源。然后用连接字段作为key,其余部分和新加的标志作为value,最后进行输出。
Reduce端的主要工作:在reduce端以连接字段作为key的分组已经完成,我们只需要在每一个分组当中将那些来源于不同文件的记录(在map阶段已经打标志)分开,最后进行合并就ok了。
2、Reduce join案例实操
需求
表8-1 订单文本order.txt
orderid | productName | userid |
20010203001 | 手机 | 19156 |
20010203002 | 电脑 | 19157 |
20010203003 | 平板 | 19158 |
20010203004 | 手表 | 19156 |
20010203005 | 手环 | 19157 |
20010203006 | 耳机 | 19158 |
表8-2 用户信息文本user.txt
userid | username |
19156 | 张三 |
19157 | 李四 |
19158 | 王五 |
sql:select * from order o join user u on o.userid=u.userid; 笛卡尔积,join条件:order.userid=user.userid;
将用户信息文本中的用户名称数据根据userid合并到订单文本中。
表8-3 最终数据形式
orderid | productName | username |
20010203004 | 手表 | 张三 |
20010203001 | 手机 | 张三 |
20010203005 | 手环 | 李四 |
20010203002 | 电脑 | 李四 |
20010203006 | 耳机 | 王五 |
20010203003 | 平板 | 王五 |
order.txt
20010203001 手机 19156
20010203002 电脑 19157
20010203003 平板 19158
20010203004 手表 19156
20010203005 手环 19157
20010203006 耳机 19158
user.txt
19156 张三
19157 李四
19158 王五
通过将关联条件作为map输出的key,将两表满足join条件的数据(包含数据来源于哪一个文件的标识),发往同一个reduce task,在reduce中进行数据的串联,如图所示:
(2)代码实现
a. 创建订单和用户合并后的bean类
package cn.bigdata.reducejoin;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.Writable;
public class OrderBean implements Writable{
//订单id
private long orderid;
//商品名称
private String productName;
//用户id
private int userid;
//用户名称
private String username;
//标记,如果该bean封装的是来自订单的数据,则该属性置为0,用户信息数据,置为1
private String flag;
public OrderBean() {
super();
// TODO Auto-generated constructor stub
}
public OrderBean(int orderid, String productName, int userid, String username, String flag) {
super();
this.orderid = orderid;
this.productName = productName;
this.userid = userid;
this.username = username;
this.flag = flag;
}
public long getOrderid() {
return orderid;
}
public void setOrderid(long orderid) {
this.orderid = orderid;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public int getUserid() {
return userid;
}
public void setUserid(int userid) {
this.userid = userid;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getFlag() {
return flag;
}
public void setFlag(String flag) {
this.flag = flag;
}
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(orderid);
out.writeUTF(productName);
out.writeInt(userid);
out.writeUTF(username);
out.writeUTF(flag);
}
@Override
public void readFields(DataInput in) throws IOException {
this.orderid = in.readLong();
this.productName = in.readUTF();
this.userid = in.readInt();
this.username = in.readUTF();
this.flag = in.readUTF();
}
@Override
public String toString() {
return orderid+"\t"+productName+"\t"+username;
}
}
b. 编写Mapper程序
package cn.bigdata.reducejoin;
import java.io.IOException;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
public class ReduceJoinMapper extends Mapper<LongWritable, Text, LongWritable, OrderBean> {
LongWritable k = new LongWritable();
OrderBean v = new OrderBean();
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
// 1 从切片中取出文件名称,根据文件名称区分是订单文本还是用户文本
FileSplit fs = (FileSplit) context.getInputSplit();
String filename = fs.getPath().getName();
// 2 将每行转换成字符串
String line = value.toString();
// 3 按照tab键切割
String[] split = line.split("\t");
// 4 封装kv数据
//20010203001 手机 19156
if(filename.startsWith("order.txt")){//订单文本处理
v.setOrderid(Long.parseLong(split[0]));
v.setProductName(split[1]);
v.setUserid(Integer.parseInt(split[2]));
v.setUsername("");
v.setFlag("0");
//19156 张三
}else{//用户文本处理
v.setOrderid(0);
v.setProductName("");
v.setUserid(Integer.parseInt(split[0]));
v.setUsername(split[1]);
v.setFlag("1");
}
k.set(v.getUserid());
// 5 将kv写出 userid为key,bean为value
context.write(k, v);
}
}
c. 编写Reducer程序
package cn.bigdata.reducejoin;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;
public class ReduceJoinReduce extends Reducer<LongWritable, OrderBean, OrderBean, NullWritable>{
@Override
protected void reduce(LongWritable key, Iterable<OrderBean> values,Context context)
throws IOException, InterruptedException {
//<19156,20010203001 手机 19156>
//<19156,20010203004 智能手表 19156>
//<19156,19156 张三>
List<OrderBean> orders = new ArrayList<OrderBean>();
OrderBean user = new OrderBean();
// 1 循环所有的values,挑出所有的订单放到集合,挑出用户数据放到user对象
for (OrderBean ss : values) {
if("0".equals(ss.getFlag())){//该bean封装的是订单数据
OrderBean order = new OrderBean();
try {
BeanUtils.copyProperties(order, ss);
orders.add(order);
} catch (Exception e) {
e.printStackTrace();
}
}else{ //该bean封装的是用户数据
try {
BeanUtils.copyProperties(user, ss);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 2 从user对象中取出用户名称设置到所有的订单bean里面,封装kv并写出
for (OrderBean ss : orders) {
ss.setUsername(user.getUsername());
context.write(ss, NullWritable.get());
}
}
}
d. 编写Driver程序
package cn.bigdata.reducejoin;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class ReduceJoinDriver {
public static void main(String[] args) throws Exception {
// 1 获取配置信息以及封装任务
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 2 设置jar加载路径
job.setJarByClass(ReduceJoinDriver.class);
// 3 设置map和reduce类
job.setMapperClass(ReduceJoinMapper.class);
job.setReducerClass(ReduceJoinReduce.class);
// 4 设置map输出
job.setMapOutputKeyClass(LongWritable.class);
job.setMapOutputValueClass(OrderBean.class);
// 5 设置Reduce输出
job.setOutputKeyClass(OrderBean.class);
job.setOutputValueClass(NullWritable.class);
// 6 设置输入和输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 7 提交
job.waitForCompletion(true);
}
}
运行程序查看结果:
20010203004 手表 张三
20010203001 手机 张三
20010203005 手环 李四
20010203002 电脑 李四
20010203006 耳机 王五
20010203003 平板 王五
缺点:这种方式中,合并的操作是在reduce阶段完成,reduce端的处理压力太大(数据倾斜),map节点的运算负载则很低,资源利用率不高,且在reduce阶段极易产生数据倾斜(同一个reduce接收到的数据量很大)。
1, map阶段没有对数据瘦身,shuffle的网络传输和排序性能很低。
2, reduce端对订单处理,很耗内存,容易导致OOM。
解决方案: map端实现数据合并
3、Map join(合并)
(1)使用场景
一张表十分小、一张表很大。
MapJoin 适用于有一份数据较小的连接情况。做法是每个Map Task直接把该小份数据直接全部加载到内存当中。然后大份数据就作为 MapTask 的输入,对 map()方法的每次输入都去内存当中直接去匹配join。然后把连接结果按 key 输出,这种方法要使用 hadoop中的 DistributedCache 把小份数据分布到各个计算节点,每个 maptask 执行任务的节点都需要加载该数据到内存。
(2)解决方案
在map端缓存多张表,提前处理业务逻辑,这样增加map端业务,减少reduce端数据的压力,尽可能的减少数据倾斜。
(3)具体办法:采用distributedcache
a) 在mapper的setup阶段,将文件读取到缓存集合中。
b) 在驱动函数中加载缓存。
ob.addCacheFile(new URI("file:///e:/cache/pd.txt"));
//如果是集群运行,需要设置 HDFS 路径
job.addCacheFile(new URI("hdfs://hadoop102:8020/cache/pd.txt"));
job.addCacheFile(new URI("file:///D:/test/mapjoinnew/user/user.txt"));// 缓存普通文件到task运行节点,如图所示:
4、Map join案例实操
(1)分析
适用于关联表中有小表的情形。
可以将小表分发到所有的map节点,这样,map节点就可以在本地对自己所读到的大表数据进行合并并输出最终结果,可以大大提高合并操作的并发度,加快处理速度。
(2)实现代码
a. 先在驱动模块中添加缓存文件
package com.bigdata.mapjoin;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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;
import java.net.URI;
public class MapJoinDriver {
public static void main(String[] args) throws Exception {
// 1 创建一个配置对象
Configuration conf = new Configuration();
// 2 通过配置对象创建一个job
Job job = Job.getInstance(conf);
// 3 设置job的mr的路径(jar包的位置)
job.setJarByClass(MapJoinDriver.class);
// 4 设置job的mapper类 reduce类
job.setMapperClass(MapJoinMapper.class);
// 5 设置job的mapper类的keyout,valueout
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
// 6 设置job的最终输出的keyout,valueout
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// 7 设置job的输入数据的路径
FileInputFormat.setInputPaths(job,new Path(args[0]));
// 8 设置job的输出数据的路径 得保证,输出目录不能事先存在,否则报错,
Path outPath = new Path(args[1]);
FileSystem fs = FileSystem.get(conf);
if(fs.exists(outPath)){
fs.delete(outPath,true);
}
FileOutputFormat.setOutputPath(job,outPath);
//job.addCacheFile(new URI("file:///D:/test/mapjoinnew/user/user.txt"));
job.addCacheFile(new URI(args[2]));
job.setNumReduceTasks(0);
// 9 提交job到yarn集群
boolean b = job.waitForCompletion(true);
System.out.println("是否运行成功:"+b);
}
}
b. 读取缓存的文件数据
package com.bigdata.mapjoin;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
public class MapJoinMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
Map<String,String> userMap = new HashMap<>();
@Override // 取出在driver端缓存的文件,即user.txt,读取每一行数据,将这些用户数据缓存在内存
// 该setup方法在任务执行之前(map方法调用之前)只调用一次,用于对任务进行初始化
protected void setup(Context context) throws IOException, InterruptedException {
// 获取缓存文件
URI[] cacheFiles = context.getCacheFiles();
URI cacheFile = cacheFiles[0];
Path path = new Path(cacheFile);
FileSystem fs = FileSystem.get(new Configuration());
FSDataInputStream open = fs.open(path);
BufferedReader br = new BufferedReader(new InputStreamReader(open, "UTF-8"));
// 读取每行数据,将用户信息写入map
String line = null;
while((line = br.readLine()) != null){
// [19156,张三]
String[] split = line.split("\t");
userMap.put(split[0],split[1]);
}
// 关闭资源
br.close();
}
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 让map方法只处理订单数据,不在去处理用户数据
// 1 读取每个订单 20010203001 手机 19156
String line = value.toString();
// 2 按照tab切分 找出userid
String[] split = line.split("\t");
// 3 根据userid,去usermap找对应的用户名称
String userid = split[2];
String username = userMap.get(userid);
// 4 将用户名称设置进订单数据
split[2] = username;
String join = StringUtils.join(split, "\t");
context.write(new Text(join),NullWritable.get());
}
}