Hadoop-MapReduce-ReduceJoin和MapJoin分析及案例实操-连载中

Join多种应用

1 Reduce Join

在这里插入图片描述

2 Reduce Join案例实操

1)需求

订单数据表t_order(order.txt)

id pid amount
1001 01 1
1002 02 2
1003 03 3
1004 01 4
1005 02 5
1006 03 6

商品信息表t_product(product.txt)

pid pname
01 小米
02 华为
03 格力

将商品信息表中数据根据商品pid合并到订单数据表中。

最终数据形式

id pname amount
1001 小米 1
1004 小米 4
1002 华为 2
1005 华为 5
1003 格力 3
1006 格力 6

2)需求分析

通过将关联条件作为 Map 输出的 key,将两表满足 Join 条件的数据并携带数据所来源的文件信息,发往同一个ReduceTask,在Reduce中进行数据的串联。

在这里插入图片描述
3)代码实现

(1)创建商品和订单合并后的Bean类

public class TableBean implements Writable {
    private String id; //订单id
    private String pid; //商品id
    private int amount; //商品数量
    private String pname; //商品名称
    private String flag;  //标志字段,order  pd

    public TableBean() {
    }

    //get和set方法略...

    @Override
    public void write(DataOutput out) throws IOException {
        out.writeUTF(id);
        out.writeUTF(pid);
        out.writeInt(amount);
        out.writeUTF(pname);
        out.writeUTF(flag);
    }

    @Override
    public void readFields(DataInput in) throws IOException {
        id = in.readUTF();
        pid = in.readUTF();
        amount = in.readInt();
        pname = in.readUTF();
        flag = in.readUTF();
    }

    @Override
    public String toString() {
        return id + "\t" + pname + "\t" + amount;
    }
}

(2)编写TableMapper类

public class TableMapper extends Mapper<LongWritable, Text,Text,TableBean> {

    private String fileName;
    private Text outK = new Text();
    private TableBean outV = new TableBean();

    @Override
    protected void setup(Context context) throws IOException, InterruptedException {
        //在setup里面写,是因为一个maptask执行一次,而一个maptask执行的数据刚好就是一个文件
        //通过context对象获取切片信息,然后通过切片信息获取文件名称
        FileSplit fileSplit = (FileSplit) context.getInputSplit();
        fileName = fileSplit.getPath().getName();
    }

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        String line = value.toString();
        if(fileName.contains("order")){  //对order表做操作
            //1001	01	1
            String[] split = line.split("\t");
            //封装outKV
            outK.set(split[1]);
            outV.setId(split[0]);
            outV.setPid(split[1]);
            outV.setAmount(Integer.parseInt(split[2]));
            outV.setPname("");
            outV.setFlag("order");
        }else{  //对pd表做操作
            //01	小米
            String[] split = line.split("\t");
            //封装outKV
            outK.set(split[0]);
            outV.setId("");
            outV.setPid(split[0]);
            outV.setAmount(0);
            outV.setPname(split[1]);
            outV.setFlag("pd");
        }
        //写出outKV
        context.write(outK,outV);
    }
}

(3)编写TableReducer类

public class TableReducer extends Reducer<Text, TableBean, TableBean, NullWritable> {
    @Override
    protected void reduce(Text key, Iterable<TableBean> values, Context context) throws IOException, InterruptedException {
        //用来存储orderbean的数组
        ArrayList<TableBean> orderBeans = new ArrayList<>();
        //用来存pdbean
        TableBean pdBean = new TableBean();
        
        for (TableBean value : values) {
            //判断数据来自哪个表
            if("order".equals(value.getFlag())){ //订单表
                //创建一个临时TableBean对象接收value
                //因为Iterable每次迭代都是同一个对象(地址值不变),只是改变对象元素的值
                //所以需要创建临时TableBean来接收每次value的值
                TableBean tempBean = new TableBean();
                try {
                    BeanUtils.copyProperties(tempBean,value);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                orderBeans.add(tempBean);
            }else { //商品表
                try {
                    BeanUtils.copyProperties(pdBean,value);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        //遍历集合orderBeans,替换掉每个orderBean的pid为pname,然后写出
        for (TableBean orderBean : orderBeans) {
            String pname = pdBean.getPname();
            orderBean.setPname(pname);
            //写出修改后的orderBean对象
            context.write(orderBean,NullWritable.get());
        }

    }
}

(4)编写TableDriver类

public class TableDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        Job job = Job.getInstance(new Configuration());

        job.setJarByClass(TableDriver.class);
        job.setMapperClass(TableMapper.class);
        job.setReducerClass(TableReducer.class);

        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(TableBean.class);

        job.setOutputKeyClass(TableBean.class);
        job.setOutputValueClass(NullWritable.class);

        FileInputFormat.setInputPaths(job, new Path("D:\\input\\inputtable"));
        FileOutputFormat.setOutputPath(job, new Path("D:\\hadoop\\reducejoin4"));

        boolean b = job.waitForCompletion(true);
        System.exit(b ? 0 : 1);

    }
}

4)总结
在这里插入图片描述

3 Map Join

1)使用场景

Map Join适用于一张表十分小、一张表很大的场景。

2)优点

思考:在Reduce端处理过多的表,非常容易产生数据倾斜。怎么办?

在Map端缓存多张表,提前处理业务逻辑,这样增加Map端业务,减少Reduce端数据的压力,尽可能的减少数据倾斜。

3)具体办法:采用DistributedCache

(1)在Mapper的setup阶段,将文件读取到缓存集合中。

(2)在Driver驱动类中加载缓存。

//缓存普通文件到Task运行节点。
job.addCacheFile(new URI("file:///e:/cache/pd.txt"));

4 Map Join案例实操

1)需求

订单数据表t_order(order.txt)

id pid amount
1001 01 1
1002 02 2
1003 03 3
1004 01 4
1005 02 5
1006 03 6

商品信息表t_product(product.txt)

pid pname
01 小米
02 华为
03 格力

将商品信息表中数据根据商品pid合并到订单数据表中。

最终数据形式

id pname amount
1001 小米 1
1004 小米 4
1002 华为 2
1005 华为 5
1003 格力 3
1006 格力 6

2)需求分析

MapJoin适用于关联表中有小表的情形。
在这里插入图片描述
3)实现代码

(1)先在MapJoinDriver驱动类中添加缓存文件

public class MapJoinDriver {
    public static void main(String[] args) throws Exception {
        // 1 获取job信息
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);
        // 2 设置加载jar包路径
        job.setJarByClass(MapJoinDriver.class);
        // 3 关联mapper
        job.setMapperClass(MapJoinMapper.class);
        // 4 设置Map输出KV类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(NullWritable.class);
        // 5 设置最终输出KV类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(NullWritable.class);

        // 加载缓存数据
        job.addCacheFile(new URI("file:///D:/input/inputtablecache/pd.txt"));
        // Map端Join的逻辑不需要Reduce阶段,设置reduceTask数量为0
        job.setNumReduceTasks(0);

        // 6 设置输入输出路径
        FileInputFormat.setInputPaths(job, new Path("D:\\input\\inputtable2"));
        FileOutputFormat.setOutputPath(job, new Path("D:\\hadoop\\mapjoin"));
        // 7 提交
        boolean b = job.waitForCompletion(true);
        System.exit(b ? 0 : 1);

    }
}

(2)在MapJoinMapper类中的setup方法中读取缓存文件

public class MapJoinMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
    HashMap<String, String> pdMap = new HashMap();
    private Text outK = new Text();

    @Override
    protected void setup(Context context) throws IOException, InterruptedException {
        //获取缓存文件  在driver添加的缓存文件
        URI[] cacheFiles = context.getCacheFiles();
        //获取fs对象,是不是要创建对应的文件输入流
        FileSystem fs = FileSystem.get(context.getConfiguration());
        FSDataInputStream fis = fs.open(new Path(cacheFiles[0]));
        //因为hdfs提供的FSDataInputStream不能按行读取,所以要使用转换流包装一下,得到BufferedReader
        BufferedReader reader = new BufferedReader(new InputStreamReader(fis, "UTF-8"));

        //按行读取,逐行处理
        String line;
        while (StringUtils.isNotEmpty(line = reader.readLine())) {
            //01	小米
            String[] split = line.split("\t");
            pdMap.put(split[0], split[1]);
        }
        //关流
        IOUtils.closeStream(reader);
        fs.close();
    }

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        //获取一行数据  1001	01	1
        String orderLine = value.toString();
        String[] split = orderLine.split("\t");
        //通过切割出来的pid,去pdMap里面获取到pname
        String pname = pdMap.get(split[1]);
        //封装outK 其实就是将每一行数据的pid替换成pname  pname是去内存里面的pdMap里面获取到的
        outK.set(split[0] + "\t" + pname + "\t" + split[2]);
        //写出
        context.write(outK,NullWritable.get());
    }
}

猜你喜欢

转载自blog.csdn.net/qq_32727095/article/details/107569998
今日推荐