一。MapReduce中多表合并案例
(一)需求
用Reduce两张表加载到一张表中。例如将下面两张表变成:【1101 小米 1】,早order表中用【小米】替代【01】
(二)源文件
(三)两种不同的实现方式
1)Map端表合并(Distributedcache)
1.思路
- 适用于关联表中有小表的情形;
可以将小表分发到所有的map节点,这样,map节点就可以在本地对自己所读到的大表数据进行合并并输出最终结果,可以大大提高合并操作的并发度,加快处理速度
- 将较小的表加载到缓冲中,这样可以加载MapperReducer的效率。
//6.加载缓存数据【注意:本地的前面要夹file:///】
job.addCacheFile(new URI("file:///d:/临时测试/大数据/input/pd.txt"));
2. 代码
1. DistributedCacheMapper
package MapJoin;
import org.apache.commons.lang.StringUtils;
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.*;
import java.util.HashMap;
import java.util.Map;
public class DistributedCacheMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
Map<String,String> pdMap = new HashMap<String, String>();
/**
* 初始化方法
* 将pd.txt加载进来
*/
@Override
protected void setup(Context context) throws IOException, InterruptedException {
//1.获取要缓存的文件
BufferedReader reader = new BufferedReader(new InputStreamReader((new FileInputStream(new File("D:\\临时测试\\大数据\\input\\pd.txt")))));
//2.将数据存入缓存集合中
String line;
//导包注意:org.apache.commons.lang.StringUtils
while(!StringUtils.isEmpty(line = reader.readLine())){
//将每一行进行切割
String[] fields = line.split("\t");
pdMap.put(fields[0], fields[1]);//fields[0]-pid,fields[1]-pname
}
//3.关流
reader.close();
}
Text k = new Text();
/**
* 这里面加载的是order表中的数据,而不是pd表
*/
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1.获取一行
String line = value.toString();
//2.截取
String[] fields = line.split("\t");
//3.获取订单ID,产品ID,产品数量
String orderID = fields[0];
String pid = fields[1];
String amount = fields[2];
//4.获取商品名称
String pdname = pdMap.get(pid);
//5.拼接
k.set(orderID+"\t"+pdname+"\t"+amount);
//6.写出
context.write(k,NullWritable.get());
}
}
2.DistributedCacheDriver
package MapJoin;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.Job;
import java.net.URI;
public class DistributedCacheDriver {
public static void main(String[] args) throws Exception{
args = new String[]{"D:\\临时测试\\大数据\\input\\order.txt","D:\\临时测试\\大数据\\测试结果\\mapjoin3"};
//1.获取配置信息
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//3.设置jar包加载路径
job.setJarByClass(DistributedCacheDriver.class);
//4.加载map类
job.setMapperClass(DistributedCacheMapper.class);
//4.设置map的key和value类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
//5.设置输入数据和输出数据的路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//6.加载缓存数据【注意:本地的前面要夹file:///】
job.addCacheFile(new URI("file:///d:/临时测试/大数据/input/pd.txt"));
//7.map端join的逻辑不需要reduce阶段,设置reduceTask的数量为0
job.setNumReduceTasks(0);
//8.提交
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 :1);
}
}
2)Reduce端表合并(数据倾斜)
1.思路
通过将关联条件作为map输出的key,将两表满足join条件的数据并携带数据所来源的文件信息,发往同一个reduce task,在reduce中进行数据的串联
2. 问题:hadoop中迭代器的对象重用问题
(1)进入Reducer,reducer是相同key一起进入的,也就是第一次Reducer只进入了三行数据,当order_id都为01的时候的数据:
(2)在Reducer类的方法中,我们有这样一段代码:
List<TableBean> orderBeans = new ArrayList<>();
for(TableBean bean : values){
//0是订单表
if("0".equals(bean.getFlag())){
TableBean orderBean = new TableBean();
//将bean中的值赋给orderBean
try {
BeanUtils.copyProperties(orderBean,bean);
} catch (Exception e) {
e.printStackTrace();
}
orderBeans.add(orderBean);//将过滤后的确定是订单表的数据放到orderBeans中。
}
}
有人觉得应该可以简化成如下代码:
List<TableBean> orderBeans = new ArrayList<>();
for(TableBean bean : values){
//0是订单表
if("0".equals(bean.getFlag())){
orderBeans.add(bean);
}
}
但是这个时候最后的结果会发生错误:
(3)这个简化代码的具体运行过程是
对于key为01来说,有order表有两行数据
1001 0111004014
- 当我们第一次刚好运行完【orderBeans.add(bean);】时候,orderBeans的数据是【1004 01 4】
- 我们重新进入循环,此时的【bean】就变成了【1001 01 1】,而orderBeans的数据变成了【1001 01 1】,而不是【1004 01 4】。
- 第二次循环结束后,orderBeans的数据就变成了【1001 01 1】【1001 01 1】。
(3)解释
这里参考文章:【hadoop中迭代器的对象重用问题 】
也就是说虽然reduce方法会反复执行多次,但key和value相关的对象只有两个,reduce会反复重用这两个对象。所以如果要保存key或者value的结果,只能将其中的值取出另存或者重新clone一个对象,而不能直接赋引用。因为引用从始至终都是指向同一个对象,会影响最终结果
其他的解释:
会出现对象重用,因为用的都是一个bean变量。经过测试 ,bean变量的内存地址没有改变,只是值改变了。在orderBeans保存的时候,存的就是对象bean的内存地址,所以最后得到了一样的结果。这个现象可能和foreach有关:foreach打印的时候,定义的就是一个bean引用接受的对象,就会出现重用的现象。
2.代码
(1)TableBean
package reducejoin;
import javafx.scene.control.Tab;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class TableBean implements Writable {
private String order_id;//订单id
private String p_id;//产品id
private int amount;//产品数量
private String pname;//产品名称
private String flag;//表的标记:标记是哪个表
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(order_id);
out.writeUTF(p_id);
out.writeInt(amount);
out.writeUTF(pname);
out.writeUTF(flag);
}
@Override
public void readFields(DataInput in) throws IOException {
this.order_id = in.readUTF();
this.p_id = in.readUTF();
this.amount = in.readInt();
this.pname = in.readUTF();
this.flag = in.readUTF();
}
@Override
public String toString() {
return order_id+"\t"+pname+"\t"+amount+"\t";
}
}
(2)TableMapper
package reducejoin;
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;
import java.io.IOException;
//<偏移量,数据文本,p_id,bean>
public class TableMapper extends Mapper<LongWritable, Text, Text, TableBean>{
Text k = new Text();
TableBean v = new TableBean();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1.通过context找到InputSplit的子类FileSplit
FileSplit split = (FileSplit)context.getInputSplit();
//2.获取文件命名
String name = split.getPath().getName();
//3.获取一行
String line = value.toString();
//4.按照不同的表,将数据填充.因为p_id是两张表公有的,所以要将它设为key
if(name.startsWith("order")){
String[] fields = line.split("\t");
v.setOrder_id(fields[0]);
v.setP_id(fields[1]);
v.setAmount(Integer.valueOf(fields[2]));
v.setPname(""); //订单表中没有pname,以空做处理
v.setFlag("0");
k.set(fields[1]);//订单表的flag设置为0
}else{
String[] fields = line.split("\t");
v.setOrder_id("");
v.setP_id(fields[0]);
v.setAmount(0);
v.setPname(fields[1]);
v.setFlag("1");
k.set(fields[0]);
}
//5.写出
context.write(k,v);
}
}
(3)TableReducer
package reducejoin;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import org.omg.PortableInterceptor.SYSTEM_EXCEPTION;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
public class TableReducer extends Reducer<Text, TableBean, TableBean, NullWritable> {
@Override
protected void reduce(Text key, Iterable<TableBean> values, Context context) throws IOException, InterruptedException {
List<TableBean> orderBeans = new ArrayList<>();
//这里的pdBean要单独玲出来的原因:相同key也就是order_id相同时,pd表只有一行数据,为避免数据混淆。
TableBean pdBean = new TableBean();//放到里面容易出问题
for(TableBean bean : values){
System.err.println("bean的内存地址="+bean.hashCode()+"。bean的值="+bean);
//0是订单表
if("0".equals(bean.getFlag())){
TableBean orderBean = new TableBean();
//将bean中的值赋给orderBean
try {
BeanUtils.copyProperties(orderBean,bean);
} catch (Exception e) {
e.printStackTrace();
}
orderBeans.add(orderBean);
System.err.println("orderBeans="+orderBeans);
}else{
//产品表
try {
BeanUtils.copyProperties(pdBean,bean);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//拼接
for(TableBean bean : orderBeans){
//相同key也就是order_id相同时,pd表只有一行数据,所以不用循环,直接用。
bean.setPname(pdBean.getPname());
context.write(bean,NullWritable.get());
}
}
}
(4)TableDriver
package reducejoin;
import org.apache.hadoop.conf.Configuration;
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.io.IOException;
public class TableDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
args = new String[]{"D:\\临时测试\\大数据\\input\\phone","D:\\临时测试\\大数据\\测试结果\\table6"};
//1.获取配置信息,或者job对象实例
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//2.指定本程序jar包所在的本地路径
job.setJarByClass(TableDriver.class);
//3.指定本业务job要使用的mapper/reducer业务类
job.setMapperClass(TableMapper.class);
job.setReducerClass(TableReducer.class);
//4.设置mapper的key和value类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(TableBean.class);
//5.设置reducer的key和value类型
job.setOutputKeyClass(TableBean.class);
job.setOutputValueClass(NullWritable.class);
//6.设置输入数据和输出数据的路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//7.运行
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
(四)测试