携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第9天,点击查看活动详情
在数据库中我们经常会遇到一些业务场景需要将多个表进行join实现某些特定的业务逻辑,在MapReduce中也可以实现join,这个join功能可以是在map端也可以是在reduce端。下面的案例中我们会以reduce端的join为例实现该功能。
需求:
有两个表,一个表是员工表(eid,name,deptid),另一个表是部门表(deptid,dept),想要实现的功能是经过reduce之后最后输出的是一个将员工表中的deptid替换为部门表中的dept的一个表(eid,name,dept),连个表之间通过deptid进行join。
- 数据准备
[root@hadoop301 testdata]# cat employee.txt
001,Tina,d03
002,Sherry,d01
003,Bob,d01
004,Sam,d02
005,Mohan,d01
006,Tom,d03
[root@hadoop301 testdata]# cat dept.txt
d01,marketing
d02,financial
d03,production
上传至HDFS:
[root@hadoop301 testdata]# hdfs dfs -mkdir /testreducejoin
[root@hadoop301 testdata]# hdfs dfs -put employee.txt /testreducejoin
[root@hadoop301 testdata]# hdfs dfs -put dept.txt /testreducejoin
新建project:
- 引入pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>wyh.test</groupId>
<artifactId>Test3ReduceJoin</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-core</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<minimizeJar>true</minimizeJar>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
- 自定义一个封装的Bean
由于我们需要把表中的各个字段一起进行传输,所以需要将其封装为一个Bean对象。
package wyh.test3.reducejoin;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class ReduceJoinBean implements Writable {
/**
* 员工表(eid,name,deptid)
* 部门表(deptid,dept)
* 需要一个字段flag标记该条数据是哪个表
*/
private String eid;
private String name;
private String deptid;
private String dept;
private String flag;
public ReduceJoinBean() {
}
public String getEid() {
return eid;
}
public void setEid(String eid) {
this.eid = eid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDeptid() {
return deptid;
}
public void setDeptid(String deptid) {
this.deptid = deptid;
}
public String getDept() {
return dept;
}
public void setDept(String dept) {
this.dept = dept;
}
public String getFlag() {
return flag;
}
public void setFlag(String flag) {
this.flag = flag;
}
//序列化
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeUTF(eid);
dataOutput.writeUTF(name);
dataOutput.writeUTF(deptid);
dataOutput.writeUTF(dept);
dataOutput.writeUTF(flag);
}
//反序列化,注意反序列化的顺序必须与序列化的属性顺序一致
@Override
public void readFields(DataInput dataInput) throws IOException {
this.eid = dataInput.readUTF();
this.name = dataInput.readUTF();
this.deptid = dataInput.readUTF();
this.dept = dataInput.readUTF();
this.flag = dataInput.readUTF();
}
@Override
public String toString() {
return eid + '\t' + name + '\t' + dept;
}
public ReduceJoinBean(String eid, String name, String deptid, String dept, String flag) {
this.eid = eid;
this.name = name;
this.deptid = deptid;
this.dept = dept;
this.flag = flag;
}
}
- 自定义Mapper
package wyh.test3.reducejoin;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.IOException;
/**
* K1:行偏移量
* V1:行文本数据
* K2:deptid(因为要用这个字段将两个表进行join)
* V2:ReduceJoinBean
*/
public class ReduceJoinMapper extends Mapper<LongWritable, Text, Text, ReduceJoinBean> {
private String fileName;
//每个MapTask只会在初始化时调用一次该方法;初始化时获取文件名,这样只需要获取一次即可,如果在map中获取的话就会是每条数据都要获取一下
@Override
protected void setup(Mapper<LongWritable, Text, Text, ReduceJoinBean>.Context context) throws IOException, InterruptedException {
//获取切片
FileSplit inputSplit = (FileSplit)context.getInputSplit();
//获取文件名
fileName = inputSplit.getPath().getName();
}
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, ReduceJoinBean>.Context context) throws IOException, InterruptedException {
//获取数据文件中的每一行数据
String raw = value.toString();
//判断文件名称
if(fileName.contains("employee")){
//员工表
//分隔行文本数据中的字段
String[] split = raw.split(",");
//key为deptid(split[2])
context.write(new Text(split[2]), new ReduceJoinBean(split[0], split[1], split[2], "", "employee"));
}else{
//部门表
String split[] = raw.split(",");
//key为deptid(split[0])
context.write(new Text(split[0]), new ReduceJoinBean("", "", split[0], split[1], "dept"));
}
}
}
- 自定义Reducer
package wyh.test3.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 java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
public class ReduceJoinReducer extends Reducer<Text, ReduceJoinBean, ReduceJoinBean, NullWritable> {
@Override
protected void reduce(Text key, Iterable<ReduceJoinBean> values, Reducer<Text, ReduceJoinBean, ReduceJoinBean, NullWritable>.Context context) throws IOException, InterruptedException {
/**
* 经过map之后,两个表中相同key的数据会被放在同一个value集合中,此时需要遍历value集合
*/
//初始化一个存放相同key的员工表记录的集合
ArrayList<ReduceJoinBean> employeeList = new ArrayList<>();
//初始化一个与上面key相同的部门表记录的对象(因为每个key/deptid在部门表中只会有一条记录,所以这里不需要集合)
ReduceJoinBean deptBean = new ReduceJoinBean();
//遍历map之后的结果中value集合
for (ReduceJoinBean value : values) {
//在遍历value集合中,可能是员工表中的记录也可能是部门表中的记录
if("employee".equals(value.getFlag())){//员工表
/**
* 由于在hadoop的集合中他进行了一定的改动,当我们使用list.add()时,每次都会覆盖前一次的内容,
* 而不是像java中那样每次添加新的数据,所以我们这里对每一次遍历出来的记录都重新初始化一个对象,
* 这样每次把新的对象都add进去时就不会覆盖之前的数据。
*/
ReduceJoinBean tmpEmployeeBean = new ReduceJoinBean();
try {
//由于values集合中存储的也是ReduceJoinBean对象,所以可以使用BeanUtils工具类的copyProperties()将values集合中遍历出来的对象的属性值赋值给我们每次新建的对象。
BeanUtils.copyProperties(tmpEmployeeBean, value);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
//将每次遍历后填充的新对象放入我们初始化的集合中
employeeList.add(tmpEmployeeBean);
}else {
try {
//部门表不需要循环,因为每个key在部门表中只有一条记录,直接把这条记录的对象属性拷贝给初始化的对象即可
BeanUtils.copyProperties(deptBean, value);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
//遍历初始化的集合,将集合中的每个对象都赋值部门名称
for (ReduceJoinBean reduceJoinBean : employeeList) {
//赋值
reduceJoinBean.setDept(deptBean.getDept());
//写出
context.write(reduceJoinBean, NullWritable.get());
}
}
}
- 自定义主类
package wyh.test3.reducejoin;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
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.Job;
import org.apache.hadoop.mapreduce.lib.input.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
public class ReduceJonMain extends Configured implements Tool {
@Override
public int run(String[] strings) throws Exception {
/**
*创建job任务对象,参数一为Configuration类型的对象,需要注意的是在同一个job任务中,上下文必须使用同一个Configuration对象,
* 而下面的main()中已经创建了Configuration对象,所以必须要使用main()中的configuration,而这个对象在下面的run方法中
* 其实已经被保存在了Configured类中,因为Configured类中有一个私有变量是Configuration对象。所以这里我们就是要想办法拿到Configured类中的configuration,
* 而当前我们的自定义类WordCountMain又是Configured的子类,所以我们可以通过super对象来调用其父类Configured的configuration对象。
* 参数二为自定义的job name。
*/
Job job = Job.getInstance(super.getConf(), "testCombineJob");
//!!!!!!!!!! 集群必须要设置 !!!!!!!!
job.setJarByClass(ReduceJonMain.class);
//配置job具体要执行的任务步骤
//指定要读取的文件的路径,这里写了目录,就会将该目录下的所有文件都读取到
FileInputFormat.setInputPaths(job, new Path("hdfs://hadoop301:8020/testreducejoin"));
//指定map处理逻辑类
job.setMapperClass(ReduceJoinMapper.class);
//指定map阶段输出的k2类型
job.setMapOutputKeyClass(Text.class);
//指定map阶段输出的v2类型
job.setMapOutputValueClass(ReduceJoinBean.class);
//指定reduce处理逻辑类
job.setReducerClass(ReduceJoinReducer.class);
//设置reduce之后输出的k3类型
job.setOutputKeyClass(ReduceJoinBean.class);
//设置reduce之后输出的v3类型
job.setOutputValueClass(NullWritable.class);
//指定结果输出路径,该目录必须是不存在的目录(如已存在该目录,则会报错),它会自动帮我们创建
FileOutputFormat.setOutputPath(job, new Path("hdfs://hadoop301:8020/testreducejoinoutput"));
//返回执行状态
boolean status = job.waitForCompletion(true);
//使用三目运算,将布尔类型的返回值转换为整型返回值,其实这个地方的整型返回值就是返回给了下面main()中的runStatus
return status ? 0:1;
}
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
/**
* 参数一是一个Configuration对象,参数二是Tool的实现类对象,参数三是一个String类型的数组参数,可以直接使用main()中的参数args.
* 返回值是一个整型的值,这个值代表了当前这个任务执行的状态.
* 调用ToolRunner的run方法启动job任务.
*/
int runStatus = ToolRunner.run(configuration, new ReduceJonMain(), args);
/**
* 任务执行完成后退出,根据上面状态值进行退出,如果任务执行是成功的,那么就是成功退出,如果任务是失败的,就是失败退出
*/
System.exit(runStatus);
}
}
- 打包并上传至服务器
- 运行jar
[root@hadoop301 testjar]# hadoop jar Test3ReduceJoin-1.0-SNAPSHOT.jar wyh.test3.reducejoin.ReduceJonMain
[root@hadoop301 testjar]# hdfs dfs -cat /testreducejoinoutput/part-r-00000
可以看到原来员工表中的部门编号被替换成部门名称并输出。
这样就简单地实现了reduce端的join案例。