打怪升级之小白的大数据之旅(五十一)<MapReduce框架原理三:OutputFormat&Join>

打怪升级之小白的大数据之旅(五十一)

MapReduce框架原理三:OutputFormat&Join

上次回顾

上一章,我们学习了MapReduce框架中的shuffle机制,本章节是MapReduce中的最后一个模块OutputFormat,它的原理和前面我们学的InputFormat一样…本章还会为大家带来一个实际一点的需求:Join

OutputFormat

OutputFormat和InputFormat原理一样,底层都是流的方式来完成对数据的操作,首先介绍一下OutputFormat的继承树

OutputFormat继承树
	|----OutputFormat(抽象类)
		|-----FileOutputFormat(抽象类)
			|-----TextOutputFormat(默认使用的OutputFormat类)
			|-----SequenceFileOutputFormat(将SequenceFileOutputFormat的输出作为后续的MapReduce任务的输入)
			

OutputFormat同样默认使用的是子类的TextOutputFormat,TextOutputFormat使用了RecordWriter的读取方法,使用LineRecordWriter来对数据进行一行一行的写出操作,下面是各个类中的源码

  • OutputFormat(抽象类)

    	  /*
    		用来获取RecordWriter对象,该对象是用来写数据的。
    	  */
    	  public abstract RecordWriter<K, V> 
        getRecordWriter(TaskAttemptContext context
                        ) throws IOException, InterruptedException;
    					
    	/*
    		用来检查输出的一些参数 :比如 ①检查输出路径是否设置了 ②输出路径是否存在
    	*/
    	  public abstract void checkOutputSpecs(JobContext context
                                            ) throws IOException, 
                                                     InterruptedException;
    
  • FileOutputFormat(抽象类)

    1.重写了checkOutputSpecs方法,在该方法中做了如下操作:
    	①检查输出路径是否设置了 ②输出路径是否存在
    
  • TextOutputFormat(默认使用的OutputFormat类

    /*
    	重写了父类的getRecordWriter方法,该方法返回了LineRecordWriter的对象。
    		该对象是真正用来写数据的对象。
    		LineRecordWrite是RecordWrite的子类。
    */
     public RecordWriter<K, V> 
             getRecordWriter(TaskAttemptContext job
                             ) throws IOException, InterruptedException {
       
          return new LineRecordWriter<>(fileOut, keyValueSeparator);
      }	
    

自定义OutputFormat

  • 我们知道TextOutputFormat是默认的类,它是一行一行的完成数据的写出操作,当它不能满足我们的需求,我们就需要自定义OutputFormat
  • 自定义OutputFormat步骤
    • 因为是替代TextOutputFormat,所以我们自己实现继承FileOutputFormat就好了
    • 继承之后,我们的数据写出也需要自定义,所以我们还需要一个自定义的类来继承RecordWriter,用于具体改写输出数据的方法,下面我通过案例来介绍自定义OutputFormat的用法

自定义OutputFormat实操

  • 需求:
    • 我希望将下面的log.txt通过自定义OutputFormat,将百度的网址专门用于一个文件保存(baidu.txt),其他数据用一个文件保存(other.txt)
  • 测试数据: log.txt
http://www.baidu.com
http://www.google.com
http://cn.bing.com
http://www.atguigu.com
http://www.sohu.com
http://www.sina.com
http://www.sin2a.com
http://www.sin2desa.com
http://www.sindsafa.com
  • 编写思路

    • 我们要完成上面的文件输出,首先就要考虑在哪里进行网址的过滤,map和reduce中,其实两个模块都用不到,我们直接定义OutputFormat,在文件写出的时候进行判断即可,所以我们就可以有三个类完成这个工作
    • AddressDriver 用于主程序,注册job,加载配置文件、设置OutputFormat、以及提交job等工作
    • MyOutputFormat类,用于替代MapReduce的默认TextOutputFormat类
    • MyRecordWriter类,用于完成写出数据的方法,在这个类中,我们完成网址的过滤以及文件的输出
  • AddressDriver

    package com.company.outputformat;
    
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.LongWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Job;
    import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
    import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
    
    import java.io.IOException;
    
    public class AddressDriver {
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
    		// 建立job对象
            Job job = Job.getInstance(new Configuration());
    		// 设置新的OutputFormat子类
            job.setOutputFormatClass(MyOutputFormat.class);
    		// 设置最终的输出数据类型
            job.setOutputKeyClass(LongWritable.class);
            job.setOutputValueClass(Text.class);
    		// 文件输入路径
            FileInputFormat.setInputPaths(job,new Path("D:\\io\\input7"));
            //设置输出路径
            FileOutputFormat.setOutputPath(job,new Path("D:\\io\\output7"));
    		// 提交job
            job.waitForCompletion(true);
    
        }
    }
    
  • MyOutputFormat

    package com.company.outputformat;
    
    import org.apache.hadoop.io.LongWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.*;
    import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
    
    import java.io.IOException;
    
    /*
        自定义OutputFormat类
    
        思考:
            1.继承谁?
                 FileOutputFormat<K, V>
            2.泛型是什么?
                 因为没有Reducer和Mapper所以K,V是InputFormat的K,V
    
     */
    public class MyOutputFormat extends FileOutputFormat<LongWritable, Text> {
    
        @Override
        public RecordWriter<LongWritable, Text>
            getRecordWriter(TaskAttemptContext job)
                throws IOException, InterruptedException {
    
            //自定义RecordWriter
            return new MyRecordWriter(job);
        }
    }
    
    
  • MyRecordWriter

    package com.company.outputformat;
    
    import org.apache.hadoop.conf.Configuration;
    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.LongWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.RecordWriter;
    import org.apache.hadoop.mapreduce.TaskAttemptContext;
    import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
    
    import java.io.IOException;
    
    /*
        K,V和OutputFormat的K,V相同
     */
    public class MyRecordWriter extends RecordWriter<LongWritable, Text> {
        private FSDataOutputStream other;
        private FSDataOutputStream atguigu;
    
        public MyRecordWriter(TaskAttemptContext job){
            try {
                //创建流---通过FileSystem创建流
                FileSystem fs = FileSystem.get(job.getConfiguration());
                //获取输出路径
                Path outputPath = FileOutputFormat.getOutputPath(job);
                //创建流
                other =
                        fs.create(new Path(outputPath, "other.log"));
                atguigu =
                        fs.create(new Path(outputPath, "atguigu.log"));
    
            }catch (Exception e){
                //终止程序的运行
                IOUtils.closeStream(other);
                IOUtils.closeStream(atguigu);
                //将编译时异常转换为运行时异常(思想)
                throw new RuntimeException(e.getMessage());
            }finally {
                //程序终止要做的工作。
            }
    
        }
    
        /**
         * 将数据写出去
         * 该方法是在被循环调用,每调用一次会传入一行数据。
         * @param key 偏移量
         * @param value 数据
         * @throws IOException
         * @throws InterruptedException
         */
        @Override
        public void write(LongWritable key, Text value)
                throws IOException, InterruptedException {
            String line = value.toString() + "\n";
            //1.判断
            if (line.contains("atguigu")){//包含atguigu的网址
                //2.将数据写出
                atguigu.write(line.getBytes());
            }else{//其它
                other.write(line.getBytes());
            }
        }
    
        /**
         * 用来关闭资源
         * 该方法只会被调用一次,是在最后的时候被调用的
         * @param context
         * @throws IOException
         * @throws InterruptedException
         */
        @Override
        public void close(TaskAttemptContext context)
                throws IOException, InterruptedException {
            //关流
            IOUtils.closeStream(atguigu);
            IOUtils.closeStream(other);
        }
    }
    
    

Join多种应用

在学习mysql的时候,我们学习了join这个函数,它可以连接两张表,如果我们在开发中也需要对表进行合并呢?在MapReduce中有两种实现的方法,一种是在map阶段进行join,一种是在reduce阶段进行join

Reduce Join

工作原理

  • Map阶段
    • 为来自不同表或文件的key/value对,打上标签来区分不同来源,然后用连接字段作为key,其余部分和新加的标志作为value.然后输出k,v交给reduce
  • reduce阶段
    • reduce接收的数据,其以连接字段作为key的分组工作已经完成了,所以我们只需要在每一个分组中,将前面打上标签的不同文件记录区分开,最后进行合并就可以了
    • 表述有些抽象,我们根据案例来说明
Reduce Join实例

需求,将下面的order.txt与pd.txt两个数据进行合并,并按照下面的最终数据进行输出

  • order.txt
    在这里插入图片描述
  • pd.txt
    在这里插入图片描述
  • 我们最终的效果如下:
    在这里插入图片描述

测试数据:

  • order.txt

    1001	01	1
    1002	02	2
    1003	03	3
    1004	01	4
    1005	02	5
    1006	03	6
    
  • pd.txt

    01	小米
    02	华为
    03	格力
    
  • 编写步骤

    • 我们并不能像mysql一样,一条join in语句就搞定,所以我们的编写思路很重要,通过观察,我们发现在上面的两张表中,pid可以作为连接字段,那么我们可以根据pid来获取到对应的pname从而对两个表进行合并
    • 所以我们可以通过将关联条件作为Map输出的key,将两表满足Join条件的数据并携带数据所来源的文件信息,发往同一个ReduceTask,在Reduce中进行数据的串联
      在这里插入图片描述
  • 这里面核心的就是,我们建立一个对象,里面存放两张表中所有的数据,没有的使用0或" "来填充,然后将这个对象存放到数据的key中,值使用Null类型来替代

  • 当Reduce阶段时,我们根据iterator迭代器学到的知识,来完成数据填充工作

    • 迭代器内部类似一个指针,它每next()的时候,就会将指针移动到下一个,我们迭代遍历value的时候,key的指向也会指导下一个,所以我们可以根据这个特性来完成我们需要的pname替换
  • MyGroup

    package com.company.reducejoin;
    
    import org.apache.hadoop.io.WritableComparable;
    import org.apache.hadoop.io.WritableComparator;
    
    /*
        自定义分组:
            1.如果不自定义分组,那么默认分组的方式和排序的方式相同。
            2.自定义一个类并继承WritableComparator
            3.调用父类指定构造器
     */
    public class MyGroup extends WritableComparator {
    
        public MyGroup(){
            /*
            调用父类构造器
            WritableComparator(Class<? extends WritableComparable> keyClass,
                 boolean createInstances)
    
                keyClass : key的数据的运行时类的类型。
                createInstances :是否创建实例(对象)
             */
            super(OrderBean.class,true);
        }
    
        @Override
        public int compare(WritableComparable a, WritableComparable b) {
            //指定分组方式(排序---pid)
            OrderBean oa = (OrderBean) a;
            OrderBean ob = (OrderBean) b;
            return Long.compare(oa.getPid(),ob.getPid());
        }
    }
    
    
  • OrderBean

    package com.company.reducejoin;
    
    import org.apache.hadoop.io.WritableComparable;
    
    import java.io.DataInput;
    import java.io.DataOutput;
    import java.io.IOException;
    
    public class OrderBean implements WritableComparable<OrderBean> {
        private long pid;
        private long id;
        private String amount;
        private String pname;
    
        public OrderBean() {
        }
    
        public OrderBean( long id,long pid, String amount, String pname) {
            this.pid = pid;
            this.id = id;
            this.amount = amount;
            this.pname = pname;
        }
        @Override
        public int compareTo(OrderBean o) {
            //先按照pid排序再按照pname排序
            int comparePid = Long.compare(this.pid, o.pid);
            if (comparePid == 0){//说明Pid相同再按照Pname排序
                return -this.pname.compareTo(o.pname);
            }
            return comparePid;
        }
    
        @Override
        public void write(DataOutput out) throws IOException {
            out.writeLong(id);
            out.writeLong(pid);
            out.writeUTF(pname);
            out.writeUTF(amount);
        }
    
        @Override
        public void readFields(DataInput in) throws IOException {
            id = in.readLong();
            pid = in.readLong();
            pname = in.readUTF();
            amount = in.readUTF();
        }
    
        @Override
        public String toString() {
            return id + " " + pid + " " + pname + " " + amount;
        }
    
        public long getPid() {
            return pid;
        }
    
        public void setPid(long pid) {
            this.pid = pid;
        }
    
        public long getId() {
            return id;
        }
    
        public void setId(long id) {
            this.id = id;
        }
    
        public String getAmount() {
            return amount;
        }
    
        public void setAmount(String amount) {
            this.amount = amount;
        }
    
        public String getPname() {
            return pname;
        }
    
        public void setPname(String pname) {
            this.pname = pname;
        }
    
    
    }
    
    
  • ReduceDriver

    package com.company.reducejoin;
    
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.NullWritable;
    import org.apache.hadoop.mapreduce.Job;
    import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
    import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
    
    import java.io.IOException;
    
    public class ReduceDriver {
    
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
    
            Job job = Job.getInstance(new Configuration());
            //指定自定义分组类
            job.setGroupingComparatorClass(MyGroup.class);
            job.setMapperClass(ReduceMapper.class);
            job.setReducerClass(ReduceReducer.class);
            job.setMapOutputKeyClass(OrderBean.class);
            job.setMapOutputValueClass(NullWritable.class);
            job.setOutputKeyClass(OrderBean.class);
            job.setOutputValueClass(NullWritable.class);
    
    
            FileInputFormat.setInputPaths(job,new Path("D:\\io\\input9"));
            FileOutputFormat.setOutputPath(job,new Path("D:\\io\\output9"));
    
    
            job.waitForCompletion(true);
    
    
        }
    }
    
    
  • ReduceMapper

    package com.company.reducejoin;
    
    import org.apache.hadoop.io.LongWritable;
    import org.apache.hadoop.io.NullWritable;
    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 ReduceMapper extends Mapper<LongWritable, Text,OrderBean, NullWritable> {
        private String fileName;
    
        /*
              setup方法只会在任务开始的时候调用一次。
              setup方法在map方法之前调用
              作用 :初始化
         */
        @Override
        protected void setup(Context context) throws IOException, InterruptedException {
            //通过切片信息获取文件名
            FileSplit fileSplit = (FileSplit) context.getInputSplit();
            fileName = fileSplit.getPath().getName();
        }
    
        /*
            cleanup方法只会在任务结束的时候调用一次
            cleanup方法在map方法后面调用
            作用 :关闭资源
         */
        @Override
        protected void cleanup(Context context) throws IOException, InterruptedException {
            super.cleanup(context);
        }
    
    
        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            //1.切割数据
            String line = value.toString();
            String[] lineSplit = line.split("\t");
            //2.封装K,V
            OrderBean outkey = null;
            //判断
            if ("order.txt".equals(fileName)) {
                outkey = new OrderBean(Long.parseLong(lineSplit[0]),
                        Long.parseLong(lineSplit[1]), lineSplit[2], "");
    
            }else if("pd.txt".equals(fileName)){
                outkey = new OrderBean(0,
                        Long.parseLong(lineSplit[0]), "", lineSplit[1]);
            }
            //3.写出K,V
            context.write(outkey,NullWritable.get());
        }
    }
    
    
  • ReduceReducer

    package com.company.reducejoin;
    
    import org.apache.hadoop.io.NullWritable;
    import org.apache.hadoop.mapreduce.Reducer;
    
    import java.io.IOException;
    import java.util.Iterator;
    
    public class ReduceReducer extends Reducer<OrderBean, NullWritable,OrderBean,NullWritable> {
    
        /*
                        key                             value
                    0    1   ""    小米                   null
                 1001   1     1    ""                    null
                 1004   1     4     ""                   null
    
         */
        @Override
        protected void reduce(OrderBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
            //获取iterator对象
            Iterator<NullWritable> iterator = values.iterator();
            //获取第一条数据
            iterator.next();
            //取出pname的值
            String pname = key.getPname();
            //循环剩下的所有数据并将pname进行替换
            while(iterator.hasNext()){
                //指针下移
                iterator.next();
                //设置pname的值
                key.setPname(pname);
                //写出K,V
                context.write(key,NullWritable.get());
            }
        }
    }
    
    

上面就是使用Reduce Join来完成数据的拼接工作,这里有很大的一个缺点,就是数据合并操作在Reduce阶段完成,合并压力太大,而Map缺比较清闲,在Reduce阶段特别容易产生数据倾斜(一组数据很多,另一组数据很少)

Map Join

  • 为了解决Reduce端处理过多的数据表而产生的数据倾斜,我们可以采取第二种方式,在Map端就进行数据的合并操作
  • Map Join 适用于一张表小,一张表很大的场景
  • 原理就是,它会将小的表数据写入到内存中,然后通过map阶段读取的时候完成数据表的连接工作
  • 具体步骤
    • 在Mapper的setup阶段,将文件读取缓存到内存中
    • 在驱动函数中加载缓存的数据
    • 将缓存文件放到mapTask节点
      job.addCacheFile(new URI("file://e:/cache/pd.txt"));
      

同样的,还是以上面的那个案例以及数据进行合并,我们采用Map Join的方式

Map Join实例

测试数据就不写了,上面有,我就直接上思路和代码了

编码思路

  • 直接将小表 pd.txt存入到缓存中

  • 在setup方法中,读取缓存的文件

  • 在map阶段,将数据进行切割后,进行pid比对,然后进行pname的填充
    在这里插入图片描述

  • MapDriver

    package com.company.mapjoin;
    
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.NullWritable;
    import org.apache.hadoop.mapreduce.Job;
    import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
    import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
    
    import java.io.IOException;
    import java.net.URI;
    import java.net.URISyntaxException;
    
    public class MapDriver {
    
        public static void main(String[] args) throws IOException, URISyntaxException, ClassNotFoundException, InterruptedException {
    
            Job job = Job.getInstance(new Configuration());
            //添加缓存文件-可以是多个
            job.addCacheFile(new URI("file:///D:/io/input9/pd.txt"));
    
            job.setMapperClass(MapMapper.class);
            job.setMapOutputKeyClass(OrderBean.class);
            job.setMapOutputValueClass(NullWritable.class);
    
            job.setOutputKeyClass(OrderBean.class);
            job.setOutputValueClass(NullWritable.class);
    
            FileInputFormat.setInputPaths(job,new Path("D:\\io\\input9\\order.txt"));
            FileOutputFormat.setOutputPath(job,new Path("D:\\io\\output99"));
    
            job.waitForCompletion(true);
    
    
        }
    }
    
    
  • MapMapper

    package com.company.mapjoin;
    
    import org.apache.hadoop.fs.FSDataInputStream;
    import org.apache.hadoop.fs.FileSystem;
    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.Mapper;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.net.URI;
    import java.util.HashMap;
    import java.util.Map;
    
    public class MapMapper extends Mapper<LongWritable, Text,OrderBean, NullWritable> {
        //该集合用来存放pid和pname
        private Map<String,String> map = new HashMap<String,String>();
        /*
            只在任务开始的时候执行一次。在map方法之前执行。
    
            用来读取pd.txt并缓存到内存中(放到集合中)
         */
        @Override
        protected void setup(Context context) throws IOException, InterruptedException{
            FSDataInputStream fis = null;
            FileSystem fs = null;
            try {
                //创建文件系统对象
                fs = FileSystem.get(context.getConfiguration());
                //获取缓存文件
                URI[] cacheFiles = context.getCacheFiles();
                //创建流
                fis = fs.open(new Path(cacheFiles[0]));
                //一行一行读取数据(将字节流转换成字符流,然后再使用字符缓冲流)
                BufferedReader br = new BufferedReader(new InputStreamReader(fis,"utf8"));
                //读取pd.txt中的数据
                String line = "";
                while ((line = br.readLine()) != null) {
                    //切割数据
                    String[] lineSplit = line.split("\t");
                    //将数据放入到map中
                    map.put(lineSplit[0], lineSplit[1]);
                }
            }catch (Exception e){
                throw new RuntimeException(e.getMessage());
            }finally {
                //关闭资源
                if (fs != null){
                    try {
                        fs.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
    
                if(fis != null){
                    try {
                        fis.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
    
        }
    
        /*
                map方法用来读取order.txt,并在map方法中做join
             */
        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            //切割数据
            String line = value.toString();
            String[] lineSplit = line.split("\t");
            //封装K,V
            OrderBean outkey = new OrderBean(Long.parseLong(lineSplit[0]), Long.parseLong(lineSplit[1]),
                    lineSplit[2], map.get(lineSplit[1]));
            //写出k,v
            context.write(outkey,NullWritable.get());
        }
    }
    
    
  • OrderBean

    package com.company.mapjoin;
    
    import org.apache.hadoop.io.WritableComparable;
    
    import java.io.DataInput;
    import java.io.DataOutput;
    import java.io.IOException;
    
    public class OrderBean implements WritableComparable<OrderBean> {
        private long pid;
        private long id;
        private String amount;
        private String pname;
    
        public OrderBean() {
        }
    
        public OrderBean(long id, long pid, String amount, String pname) {
            this.pid = pid;
            this.id = id;
            this.amount = amount;
            this.pname = pname;
        }
        @Override
        public int compareTo(OrderBean o) {
            //先按照pid排序再按照pname排序
            int comparePid = Long.compare(this.pid, o.pid);
            if (comparePid == 0){//说明Pid相同再按照Pname排序
                return -this.pname.compareTo(o.pname);
            }
            return comparePid;
        }
    
        @Override
        public void write(DataOutput out) throws IOException {
            out.writeLong(id);
            out.writeLong(pid);
            out.writeUTF(pname);
            out.writeUTF(amount);
        }
    
        @Override
        public void readFields(DataInput in) throws IOException {
            id = in.readLong();
            pid = in.readLong();
            pname = in.readUTF();
            amount = in.readUTF();
        }
    
        @Override
        public String toString() {
            return id + " " + pid + " " + pname + " " + amount;
        }
    
        public long getPid() {
            return pid;
        }
    
        public void setPid(long pid) {
            this.pid = pid;
        }
    
        public long getId() {
            return id;
        }
    
        public void setId(long id) {
            this.id = id;
        }
    
        public String getAmount() {
            return amount;
        }
    
        public void setAmount(String amount) {
            this.amount = amount;
        }
    
        public String getPname() {
            return pname;
        }
    
        public void setPname(String pname) {
            this.pname = pname;
        }
    
    
    }
    
    

总结

本章主要学习了OutputFormat和Join,然后我们通过案例来学习OutputFormat与Join的实际应用,当我们的需要对数据进行合并时,如果待合并的数据比较小,可以通过MapJoin加载缓存的方式,如果数据量比较大,我们可以通过Reduce Join的方式,当然了,实际需求用它们来完成合并数据的概率不那么多,如果有,我们也可用用Python的pandas来做,方便快捷,主要是为了学习MapReduce解决问题的思想…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值