大数据学习之路(17):MapReduce的join应用

258 阅读4分钟

一、前言

MapReduce的join 可以类似于sql的join ,不同的是sql是对表进行连接查询,而mapreduce的join是对文件进行连接查询。解决的问题就是不同数据的合并问题。

区别:

reduce join是在map阶段完成数据的标记,在reduce阶段完成数据的合并

map join是直接在map阶段完成数据的合并,没有reduce阶段。

二、Reduce join

2.1 工作原理

Map端的主要工作:为来自不同表或文件的k-v对,打标签,以区分不同来源的记录。然后用连接字段作为key,其余部分和新加的标志作为value,最后进行输出。

Reduce端的主要工作:在Reduce端以连接字段作为key的分组已经完成,我们只需要在每一个分组当中将那些来源不同文件的记录分开,最后进行合并就ok了。

2.2 案例实操

需求:两个文件,将两文件通过pid关联,合并到新的订单数据表,含有商品名。

order

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


pd

pid     pname
01	小米
02	华为
03	格力

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

orderbean

@Data
public class OrderBean implements Writable{
        //订单id
        private String orderId;
        //商品id
        private String pid;
        //订单数
        private Integer amount;
        //商品名称
        private String pname;
        //分类
        private String flag;

        public OrderBean(){

        }

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

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


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

Mapper

/**
 * 所有类型文件,都以公共的字段pid 来关联
 */
public class OrderMapper extends Mapper<LongWritable, Text,Text,OrderBean> {

    private FileSplit currentSplit;

    private Text outK=new Text();
    private OrderBean outV = new OrderBean();

    //该方法只能执行一次
    @Override
    protected void setup(Context context) throws IOException, InterruptedException {
        //获取文件名
        currentSplit=(FileSplit) context.getInputSplit();
    }

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        String[] split = value.toString().split("\t");
        //对不同数据来源分开处理,明确数据来自哪个文件
        if(currentSplit.getPath().getName().contains("order")){
            outK.set(split[1]);
            outV.setOrderId(split[0]);
            outV.setPid(split[1]);
            outV.setAmount(Integer.parseInt(split[2]));
            outV.setPname("");
            outV.setFlag("order");
        }else{
            outK.set(split[0]);
            outV.setOrderId("");
            outV.setPid(split[0]);
            outV.setPname(split[1]);
            outV.setAmount(0);
            outV.setFlag("pd");
        }
        context.write(outK,outV);
    }
}

Reduce

public class OrderReducer extends Reducer<Text,OrderBean,OrderBean, NullWritable> {
    protected List<OrderBean> list =new ArrayList<OrderBean>();
    protected OrderBean pdOrder =new OrderBean();

    @Override
    protected void reduce(Text key, Iterable<OrderBean> values, Context context) throws IOException, InterruptedException {
        //将来自order表的相同pid 数据维护到一个集合中
        //来自pd表的维护成一个对象
        for (OrderBean value : values) {
            if("order".equals(value.getFlag())){
                OrderBean orderBean = new OrderBean();
                orderBean.setOrderId(value.getOrderId());
                orderBean.setAmount(value.getAmount());
                orderBean.setFlag(value.getFlag());
                orderBean.setPname(value.getPname());
                orderBean.setPid(value.getPid());
                list.add(orderBean);
            }else{
                //来自pd
                pdOrder.setPid(value.getPid());
                pdOrder.setOrderId(value.getOrderId());
                pdOrder.setFlag(value.getFlag());
                pdOrder.setAmount(value.getAmount());
                pdOrder.setPname(value.getPname());
            }
        }
        //遍历这个list
        for (OrderBean orderBean : list) {
            orderBean.setPname(pdOrder.getPname());
            context.write(orderBean,NullWritable.get());
        }
        //一组数据处理完成后将集合清空
        list.clear();
    }
}

driver

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

        job.setJarByClass(OrderDriver.class);
        job.setMapperClass(OrderMapper.class);
        job.setReducerClass(OrderReducer.class);

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

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

        FileInputFormat.setInputPaths(job,new Path("D:/cs/join/input"));
        FileOutputFormat.setOutputPath(job,new Path("D:/cs/join/output5"));

        job.waitForCompletion(true);
    }
}

image.png

redcue join 不好的一点是所有的东西都交给reduce来做,理论上,reduce个数是很少的,如果数据很大,而map啥都不干,把所有的数据扔给redcue。会造成redcue的压力很大,而redcue的个数不同,容易造成不同reduce的处理不同。也就是数据倾斜。

三、Map join

3.1 使用场景

map join适用于 一张表十分小、一张表十分大的场景。

image.png

3.2 优点

思考:在reduce端处理过多的表,造成数据倾斜怎么办?

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

3.3 具体办法

采用DistributedCache

(1)在Mapper的setup,将文件读取到缓存集合中 (2)在驱动函数中加载缓存

3.4 实际操作

public class MapJoinDriver {
    public static void main(String[] args) throws IOException, URISyntaxException, InterruptedException, ClassNotFoundException {
        Configuration configuration = new Configuration();

        Job job =Job.getInstance(configuration);
        //在driver中 设置缓存数据 更灵活 可通过参数传入
        job.addCacheFile(new URI("file:///D:/cs/join/cache/pd.txt"));

        job.setJarByClass(MapJoinDriver.class);
        job.setMapperClass(MapJoinMapper.class);


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

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

        //摩恩是有reduce来处理过,去掉更快
        job.setNumReduceTasks(0);

        FileInputFormat.setInputPaths(job,new Path("D:/cs/join/input2"));
        FileOutputFormat.setOutputPath(job,new Path("D:/cs/join/output103"));

        job.waitForCompletion(true);
    }
}

mapper

public class MapJoinMapper extends Mapper<LongWritable,Text,Text,NullWritable> {

    private Map<String,String> pdMap=new HashMap<String, String>();

    private Text outK=new Text();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        //读取大表的数据 1001 01 1

        String line=value.toString();
        //切割
        String[] split=line.split("\t");
        //到pdmap中获取pname
        split[1]=pdMap.get(split[1]);

        //将数组中的数据转换字符串
        String s=split[0]+"\t"+split[1]+"\t"+split[2];
        outK.set(s);
        context.write(outK, NullWritable.get());
    }

    /**
     * 在处理大表之前 先把小表的数据维护到内存中
     * @param context
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    protected void setup(Context context) throws IOException, InterruptedException {

        //加载缓存文件
        URI[] cacheFiles=context.getCacheFiles();
        //从数据中获取到缓存文件
        URI cacheFile = cacheFiles[0];
        //读取文件
        FileSystem fileSystem = FileSystem.get(context.getConfiguration());
        FSDataInputStream in =fileSystem.open(new Path(cacheFile));
        //按行读取
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in,"utf8"));
        String line;
        while((line=bufferedReader.readLine())!=null){
            //切割一行数据
            // 01    小米
            String[] split = line.split("\t");
            pdMap.put(split[0],split[1]);
        }
        //关闭
        IOUtils.closeStream(bufferedReader);
    }
}