Hadoop3.x中MapReduce实现Reduce端join案例

116 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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案例。