1.概述
**在传统数据库(如:MYSQL)中,JOIN操作是非常常见且非常耗时的。而在HADOOP中进行JOIN操作,同样常见且耗时,由于Hadoop的独特设计思想,当进行JOIN操作时,有一些特殊的技巧**。
reduce side join:
假设要进行join的数据分别来自File1和File2.
reduce side join是一种最简单的join方式,其主要思想如下:
在map阶段,map函数同时读取两个文件File1和File2,为了区分两种来源的key/value数据对,对每条数据打一个标签(tag),比如:tag=0表示来自文件File1,tag=2表示来自文件File2。即:map阶段的主要任务是对不同文件中的数据打标签。
在reduce阶段,reduce函数获取key相同的来自File1和File2文件的value list, 然后对于同一个key,对File1和File2中的数据进行join(笛卡尔乘积)。即:reduce阶段进行实际的连接操作。
整个计算过程是:
(1)在map阶段,把所有记录标记成<key, value>的形式,其中key是id,value则根据来源不同取不同的形式:来源于表A的记录,value的值为"a#"+name;来源于表B的记录,value的值为"b#"+score。
(2)在reduce阶段,先把每个key下的value列表拆分为分别来自表A和表B的两部分,分别放入两个向量中。然后遍历两个向量做笛卡尔积,形成一条条最终结果。
2.实现机制
通过将关联的条件pid作为map输出的key,将两表满足join条件的数据并携带数据所来源的文件信息,发往同
一个reducetask,在reduce中进行数据的串联
待测试文件hadoop828_1.csv
hadoop828_2.csv
3. 实体类
package com.root.table;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class TableBean implements Writable {
private String country; //国家(作为唯一连接字符)
private String firstname; //名字
private String lastname; //姓氏
private String category; // 类别
private int survived; //存活
private String sex; //性别
private int age; //年龄
private String flag; //标记位,用来区分究竟是哪一个文件。
public TableBean() {
super();
}
public TableBean(String country, String firstname, String lastname, String category, int survived, String sex, int age, String flag) {
super();
this.country = country;
this.firstname = firstname;
this.lastname = lastname;
this.category = category;
this.survived = survived;
this.sex = sex;
this.age = age;
this.flag = flag;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public int getSurvived() {
return survived;
}
public void setSurvived(int survived) {
this.survived = survived;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getFlag() {
return flag;
}
public void setFlag(String flag) {
this.flag = flag;
}
public void write(DataOutput dataOutput) throws IOException {
//序列化
dataOutput.writeUTF(country);
dataOutput.writeUTF(firstname);
dataOutput.writeUTF(lastname);
dataOutput.writeUTF(sex);
dataOutput.writeInt(age);
dataOutput.writeUTF(category);
dataOutput.writeInt(survived);
dataOutput.writeUTF(flag);
}
public void readFields(DataInput dataInput) throws IOException {
//反序列化
country = dataInput.readUTF();
firstname = dataInput.readUTF();
lastname = dataInput.readUTF();
sex = dataInput.readUTF();
age = dataInput.readInt();
category = dataInput.readUTF();
survived = dataInput.readInt();
flag = dataInput.readUTF();
}
@Override
public String toString() {
return country + '\t' +
firstname + '\t' +
lastname + '\t' +
sex + "\t" + age + "\t" +
category + '\t' +
+survived + "\r\n";
}
}
实体类的flag变量用来存储文件名(做一个标记),对于map阶段的数据打上标签。
4.Map程序
**
package com.root.table;
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;
public class TableMapper extends Mapper<LongWritable, Text, Text, TableBean> {
FileSplit inputSplit;
String file_name;
Text k = new Text();
TableBean v = new TableBean();
//重写setup方法,该方法由底层run方法调用,用来获取文件名,有点初始化的味道.
@Override
protected void setup(Context context) throws IOException, InterruptedException {
// 获取文件的名称
inputSplit = (FileSplit) context.getInputSplit();
file_name = inputSplit.getPath().getName();
}
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 国家 性别 年龄 hadoop828_1.csv文件
//Sweden F 65
//国家 名字 姓氏 类别 存活 hadoop828_2.csv文件
// Estonia LEA AALISTE C 0
//获取一行
String line = value.toString();
if (file_name.startsWith("hadoop828_1")) {
String[] fields = line.split(",");
//封装k和v
k.set(fields[0]);
v.setCountry(fields[0]);
v.setSex(fields[1]);
v.setAge(Integer.parseInt(fields[2]));
v.setFirstname("");
v.setLastname("");
v.setCategory("");
v.setSurvived(0);
v.setFlag("hadoop828_1");
} else {
String[] fields = line.split(",");
//封装k和v
k.set(fields[0]);
v.setCountry(fields[0]);
v.setSex("");
v.setAge(0);
v.setFirstname(fields[1]);
v.setLastname(fields[2]);
v.setCategory(fields[3]);
v.setSurvived(Integer.parseInt(fields[4]));
v.setFlag("hadoop828_2");
}
//写出
context.write(k,v);
}
}
请特别留意这个setup方法,它的目的是为了文件的名称
5.Reduce程序
package com.root.table;
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 java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
public class TableReduce extends Reducer<Text, TableBean, TableBean, NullWritable> {
@Override
protected void reduce(Text key, Iterable<TableBean> values, Context context) throws IOException, InterruptedException {
//存储hadoop828_1.csv文件数据集合
ArrayList<TableBean> atableBean = new ArrayList<TableBean>();
//存储hadoop828_2.csv文件数据集合
ArrayList<TableBean> btableBean = new ArrayList<TableBean>();
for (TableBean value : values) {
if ("hadoop828_1".equals(value.getFlag())) { //hadoop828_1.csv文件
TableBean tmp1Bean = new TableBean();
try {
BeanUtils.copyProperties(tmp1Bean, value);
atableBean.add(tmp1Bean);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
} else { //hadoop828_2.csv文件
TableBean tmp2Bean = new TableBean();
try {
BeanUtils.copyProperties(tmp2Bean, value);
btableBean.add(tmp2Bean);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
for (TableBean tableBean1 : atableBean) {
for (TableBean tableBean2:btableBean){
tableBean1.setFirstname(tableBean2.getFirstname());
tableBean1.setLastname(tableBean2.getLastname());
tableBean1.setCategory(tableBean2.getCategory());
tableBean1.setSurvived(tableBean2.getSurvived());
//写出
context.write(tableBean1, NullWritable.get());
}
}
}
}
**特别注意:
for (TableBean value : values) {
if ("hadoop828_1".equals(value.getFlag())) { //hadoop828_1.csv文件
TableBean tmp1Bean = new TableBean();
try {
BeanUtils.copyProperties(tmp1Bean, value);
atableBean.add(tmp1Bean);
**
**这里不能用value作为参数传入atableBean.add()方法里,我这块也会经常被误导,TableBean value是一个引用(储存在栈内存中), TableBean tmp1Bean = new TableBean(); tmp1Bean是一个对象(储存在堆内存中),对于这个程序来说,迭代次数value指向的对象一直在变,value的指向只取最后一次迭代的结果,new TableBean();则迭代次数n等于它在堆内存中创建了n个不同的对象. 这个就很官方:
1 、栈区( stack )— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限.
2 、堆区( heap )— 亦称动态内存分配.程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或delete释放内存。动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存.但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉,否则,我们认为发生了内存泄漏现象。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表. **
package com.root.table;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import java.io.IOException;
public class TableWriter extends RecordWriter<TableBean, NullWritable> {
FileSystem fs;
FSDataOutputStream Estonia, Latvia, Russia,Sweden;
public TableWriter(TaskAttemptContext taskAttemptContext) {
try {
//1. 获取文件系统
fs = FileSystem.get(taskAttemptContext.getConfiguration());
//2.创建输出到Estonia.csv的输出流
Estonia = fs.create(new Path("F:\\scala\\Workerhdfs\\output12\\Estonia.csv"));
//3.创建输出到Latvia.csv的输出流
Latvia = fs.create(new Path("F:\\scala\\Workerhdfs\\output12\\Latvia.csv"));
//4.创建输出到Russia.csv的输出流
Russia = fs.create(new Path("F:\\scala\\Workerhdfs\\output12\\Russia.csv"));
//5.创建输出到Sweden.csv的输出流
Sweden = fs.create(new Path("F:\\scala\\Workerhdfs\\output12\\Sweden.csv"));
} catch (IOException e) {
e.printStackTrace();
}
}
public void write(TableBean tableBean, NullWritable nullWritable) throws IOException, InterruptedException {
String line = tableBean.toString();
if (line.contains("Estonia")) {
Estonia.write(line.getBytes());
} else if (line.contains("Latvia")) {
Latvia.write(line.getBytes());
} else if (line.contains("Russia")) {
Russia.write(line.getBytes());
} else {
Sweden.write(line.getBytes());
}
}
public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
IOUtils.closeStream(Estonia);
IOUtils.closeStream(Latvia);
IOUtils.closeStream(Russia);
IOUtils.closeStream(Sweden);
}
}
根据自己的电脑(操作系统)选择合适的路径
**
package com.root.table;
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.conf.Configuration;
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[]{"F:\\scala\\Workerhdfs\\input6","F:\\scala\\Workerhdfs\\output12"};
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//2设置jar存储位置
job.setJarByClass(TableDriver.class);
//3关联Map类与Reduce类
job.setMapperClass(TableMapper.class);
job.setReducerClass(TableReduce.class);
//4.设置Mapper阶段输出数据的key和value类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(TableBean.class);
//5.设置最终输出数据的key和value类型
job.setOutputKeyClass(TableBean.class);
job.setOutputValueClass(NullWritable.class);
job.setOutputFormatClass(TableOutputFormat.class);
//6 设置输入路径和输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//7提交job
// job.submit();
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
**
效果如下:
数据很乱不要急,点工具栏中的数据
选中第一列数据
选择分列,这里按照制表符(\t)进行分列,
点击下一步
以table作为分割符,如果程序中实体类中的toString方法不是以table作为分割,请勾选其他选项
下一步 --》完成
顿时好多了,其他文件亦是如此
将程序运行于集群上.
查看文件前五行: