MapReduce--Input与Output规则

1、MR五大阶段

  • Input
    • 如果让你写一个负责读取的类?
    • 实现读取Mysql的数据:JDBC
    • 实现读取HDFS:HDFS JavaAPI
  • Map:自定义
  • Shuffle:分区、排序、分组、Combiner
  • Reduce:自定义
  • Output
    • 如果让你写一个负责输出的类?
    • 将Reduce处理好的数据写入Mysql:JDBC
    • 将Reduce处理好的数据写入HDFS:HDFS Java API

2、Input规则

所有的输入都由Input的类来决定

job.setInputFormatClass(TextInputFormat.class)
  • 默认的Input类:TextInputFormat
    • 所有的Input类都要继承自InputFormat
  • 本质上:就是将读取的API进行了封装
    • 如果我们要自定义,就要基于封装的结构来填充API而已
  • MapReduce默认提供了一些封装好的用于输入的类
    • TextInputFormat:用于读取HDFS文件,返回一个KV
      • key:文件的每一行对应个的偏移量
      • Value:这一行的内容
    • DBInputFormat:用于读取MySQL中的数据,返回一个KV
      • Sqoop:用于实现基于MapReduce读写MySQL
    • TableInputFormat:用于读取Hbase表中的数据
  • 功能:所有的InputFormat都要实现这两个功能
    • 1-将读取到的数据进行分片
    • 2-将每个分片的数据转换为KeyValue

以TextInpuFomat为例

自带的一些方法

  • createRecordReader:创建一个读取器
    • 所有的InputFormat必须调用读取器来真正实现数据的读取转为KV
    • 读取器:真正负责读取数据的类
      • LineRecordReader:一行一行的读取文件,并转换为KV
  • isSplitable:当前读取的数据是否可以分割【决定压缩类型是否可以分割构建多个分片】

数据是如何分片的?

  • getSplits:TextInpuFormat调用父类FileInputFormat来实现了分片
  • splitSize:决定了分割的大小
    计算公式:computeSplitSize(blockSize, minSize, maxSize)
return Math.max(minSize, Math.min(maxSize, blockSize));
​
Max(最小分片数,Min(最大分片数,块的大小))
|
Max(1,Min(256M,128M)) = 128M

最终的常见的规律:HDFS上一个文件块 = 一个分片 = 启动一个MapTask

  • 假设要处理的文件是300M
HDFS :	128M	128M	44M
Input: split1	split2 split3
Map: MapTask1 MapTask2 MapTask3
  • 设计的目的
    • 因为在HDFS中已经分好了
    • 只需要让MapReduce根据分块的大小,每个MapTask处理一个分块的大小
    • 避免了HDFS上数据的再合并再分割

特殊情况:假设文件130M

  • HDFS: 128M 2M
  • 允许有10%的溢出:如果文件大小超过分片大小10%以内,作为一个分片
    • 130 / 128 > 1.1
  • 只要文件小于140M,就会作为1个分片进行处理
    • Input: split1
    • Map: MapTask
    • 设计目的:避免一个MapTask处理的数据太小
  • minSize:最小分片数
    • 属性: mapreduce.input.fileinputformat.split.minsize = 0
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
|
minSize = 1
  • maxSize:最大分片数
    • 属性:mapreduce.input.fileinputformat.split.maxsize = 256M对应的字节数
long maxSize = getMaxSplitSize(job);
|
maxSize = 256M

每个分片的数据如何转换为KeyValue?

  • 每种InputFormat都要构建一个读取器
  • 每个读取器中:都有一个nextKeyValue,用于将读取到分片的数据转为KV

3、Output规则

  • 所有输出都由Output的类来决定
job.setOutputFormatClass(TextOutputFormat.class);
  • 默认的Output类:TextOutputFormat
    • 所有的Output的类都要继承自OutputFormat
  • 本质上:就是将写入API进行了封装而已
    • 如果我们要自定义,就要基于封装的结构来填充API而已
  • MapReduce默认提供了一些封装好的用于输出的类
    • TextOutputFormat:用于将结果写入HDFS
    • DBOutputFormat:用于将结果写入MySQL
    • TableOutputFormat:用于将结果写入Hbase
  • 功能将结果保存

4、自定义一个Input

问题:如果我们要处理的数据都是很多小文件怎么办?

  • MapReduce是不适合于处理小文件
    • file1:2KB
    • file2:2KB
  • 一个文件就会作为一个分片,两个小文件就有两个分片,就会启动两个MapTask
  • 这样是及其浪费资源,可能处理的时间还没启动的时间长
  • 解决:自定义一个读取器,将每个文件的内容作为一个KV传递给Map,将所有文件合并输出成为一个文件
  • 需求:自定义一个InputFormat:用于将每个小文件的内容作为一个KV
    • 将每个文件和内容合并为一个文件:构建一个文件集合
    • SequenceFIle:这种文件用于存储多个文件,里面的每条数据KV就是一个文件,记录文件的名称文件的内容
    • MapReduce中的有SquenceOutputFormat
    • 要求输出的Key必须为Text类型的文件名
    • 要求输出的Value必须为Byteswritable类型的文件数据
    • 每一个文件在SequenceFIle中都是一个KV

实现

  • 自定义InputFormat
package bigdata.hanjiaxiaozhi.cn.mr.input;import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;import java.io.IOException;/**
 * @ClassName MRUserInputFormat
 * @Description TODO 自定义的输入的类,用于将每个文件的数据变成一个KV
 * K;不存储任何东西
 * V:每个文件的数据作为一个Value
 * @Date 2020/6/2 15:28
 * @Create By     hanjiaxiaozhi
 */
public class MRUserInputFormat  extends FileInputFormat<NullWritable, BytesWritable> {/**
     * 返回一个读取器,实现数据的转换
     * @param split
     * @param context
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    public RecordReader<NullWritable, BytesWritable> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
        //构建读取器
        MRUserRecordReader mrUserRecordReader = new MRUserRecordReader();
        //调用初始化方法
        mrUserRecordReader.initialize(split,context);
        //返回读取器
        return mrUserRecordReader;
    }/**
     * 是否可分割
     * @param context
     * @param filename
     * @return
     */
    @Override
    protected boolean isSplitable(JobContext context, Path filename) {
        //一个文件作为一个KV ,不分割
        return false;
    }
}
  • 自定义读取器
package bigdata.hanjiaxiaozhi.cn.mr.input;import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;import java.io.IOException;/**
 * @ClassName MRUserRecordReader
 * @Description TODO 自定义的读取器,用于实现将每个分片【就是每个文件】变成一个KV返回
 *
 * 通过HDFSAPI,读取文件数据,封装成KV
 *  将每个文件的内容做Value返回
 *
 * @Date 2020/6/2 15:34
 * @Create By     hanjiaxiaozhi
 */
public class MRUserRecordReader extends RecordReader<NullWritable, BytesWritable> {
    //构建需要返回的KV
    NullWritable key = NullWritable.get();
    BytesWritable value = new BytesWritable();//将每个文件的内容作为这个对象的值
    //构建全局的Conf对象
    Configuration conf = null;
    //构建全局的分片信息
    FileSplit fileSplit = null;
    //设置标志变量
    boolean flag = false;
​
​
    /**
     * 初始化方法,只被调用一次
     * @param split:分片中记录了这个分片的对应的文件信息
     * @param context:上下文对象,获取到当前程序的conf对象
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
        conf = context.getConfiguration();
        fileSplit = (FileSplit) split;
    }/**
     * 这个方法,用于获取分片【就是一个文件】中的数据,将分片的数据变成KV
     *      这个方法的返回值
     *          true:还有下一条,保存当前条,继续对下一条进行处理
     *          false:没有下一条了
     *          注意:每个分片第一次被调用 时,必须返回true,不然当前KV会被丢掉
     *          由于我们一个分片就是一个文件,只构建一个KV,第二次必须返回false
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    public boolean nextKeyValue() throws IOException, InterruptedException {
        //todo:核心的目标,读取当前分片就是文件的内容,塞到BytesWritable中,让value有值
        if(!flag){
            //构建一个HDFS文件系统
            FileSystem hdfs = FileSystem.get(conf);
            //打开当前这个分片的文件
            FSDataInputStream open = hdfs.open(fileSplit.getPath());
            //将这个输入流【这个小文件的数据】放入字节数组中
            byte[] bytes = new byte[(int) fileSplit.getLength()];
            IOUtils.readFully(open,bytes,0, (int) fileSplit.getLength());
            //给Value赋值:让Value得到这个文件的所有数据
            this.value.set(bytes,0, (int) fileSplit.getLength());
            //关闭资源
            open.close();
            hdfs.close();
            //修改标记
            flag = true;
            //返回
            return true;
        }
        //第二次返回false,表示整个分片读取完毕
        return false;
    }//返回Key
    @Override
    public NullWritable getCurrentKey() throws IOException, InterruptedException {
        return key;
    }//返回Value
    @Override
    public BytesWritable getCurrentValue() throws IOException, InterruptedException {
        return value;
    }//获取进度方法
    @Override
    public float getProgress() throws IOException, InterruptedException {
        return 0;
    }//用于关闭释放资源
    @Override
    public void close() throws IOException {}
}
  • MR
package bigdata.hanjiaxiaozhi.cn.mr.input;import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
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.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;import java.io.IOException;/**
 * @ClassName MRDriver
 * @Description TODO 实现将多个小文件合并为一个SequenceFIle
 * @Date 2020/5/30 10:34
 * @Create By     hanjiaxiaozhi
 */
public class MRUserInput extends Configured implements Tool {/**
     * 用于将Job的代码封装
     * @param args
     * @return
     * @throws Exception
     */
    @Override
    public int run(String[] args) throws Exception {
        //todo:1-构建一个Job
        Job job = Job.getInstance(this.getConf(),"model");//构建Job对象,调用父类的getconf获取属性的配置
        job.setJarByClass(MRUserInput.class);//指定可以运行的类型
        //todo:2-配置这个Job
        //input
        job.setInputFormatClass(MRUserInputFormat.class);//设置输入的类的类型,默认就是TextInputFormat
        Path inputPath = new Path("datas/inputformat");//用程序的第一个参数做为第一个输入路径
        //设置的路径可以给目录,也可以给定文件,如果给定目录,会将目录中所有文件作为输入,但是目录中不能包含子目录
        MRUserInputFormat.setInputPaths(job,inputPath);//为当前job设置输入的路径//map
        job.setMapperClass(MRMapper.class);//设置Mapper的类,需要调用对应的map方法
        job.setMapOutputKeyClass(Text.class);//设置Mapper输出的key类型
        job.setMapOutputValueClass(BytesWritable.class);//设置Mapper输出的value类型//shuffle
//        job.setPartitionerClass(HashPartitioner.class);//自定义分区
//        job.setGroupingComparatorClass(null);//自定义分组的方式
//        job.setSortComparatorClass(null);//自定义排序的方式//reduce
//        job.setReducerClass(MRReducer.class);//设置Reduce的类,需要调用对应的reduce方法
        job.setOutputKeyClass(Text.class);//文件名
        job.setOutputValueClass(BytesWritable.class);//文件内容
        job.setNumReduceTasks(1);//设置ReduceTask的个数,默认为1//output:输出目录默认不能提前存在
        job.setOutputFormatClass(SequenceFileOutputFormat.class);//设置输出的类,默认我诶TextOutputFormat
        Path outputPath = new Path("datas/output/inputformat");//用程序的第三个参数作为输出
        //解决输出目录提前存在,不能运行的问题,提前将目前删掉
        //构建一个HDFS的文件系统
        FileSystem hdfs = FileSystem.get(this.getConf());
        //判断输出目录是否存在,如果存在就删除
        if(hdfs.exists(outputPath)){
            hdfs.delete(outputPath,true);
        }
        SequenceFileOutputFormat.setOutputPath(job,outputPath);//为当前Job设置输出的路径//todo:3-提交运行Job
        return job.waitForCompletion(true) ? 0:-1;
    }/**
     * 程序的入口,调用run方法
     * @param args
     */
    public static void main(String[] args) throws Exception {
        //构建一个Configuration对象,用于管理这个程序所有配置,工作会定义很多自己的配置
        Configuration conf = new Configuration();
        //t通过Toolruner的run方法调用当前类的run方法
        int status = ToolRunner.run(conf, new MRUserInput(), args);
        //退出程序
        System.exit(status);
    }
​
​
    /**
     * @ClassName MRMapper
     * @Description TODO 这是MapReduce模板的Map类
     *      输入的KV类型:由inputformat决定,默认是TextInputFormat
     *      输出的KV类型:由map方法中谁作为key,谁作为Value决定
     */
    public static class MRMapper extends Mapper<NullWritable, BytesWritable, Text,BytesWritable> {
​
        Text outputKey = new Text();/**
         * 通过自定义的InputFormat返回的类型
         * @param key
         * @param value:每一个Value,就是每一个文件的所有内容
         * @param context
         * @throws IOException
         * @throws InterruptedException
         */
        @Override
        protected void map(NullWritable key, BytesWritable value, Context context) throws IOException, InterruptedException {
            //文件名作为Key
            FileSplit fileSplit = (FileSplit) context.getInputSplit();
            //获取这条数据对应的文件名
            String name = fileSplit.getPath().getName();
            this.outputKey.set(name);
            //文件的内容作为value
            //输出
            context.write(this.outputKey,value);
        }
    }
​
​
​
    /**
     * @ClassName MRReducer
     * @Description TODO MapReduce模板的Reducer的类
     *      输入的KV类型:由Map的输出决定,保持一致
     *      输出的KV类型:由reduce方法中谁作为key,谁作为Value决定
     */
    public static class MRReducer extends Reducer<NullWritable,NullWritable,NullWritable,NullWritable> {
        @Override
        protected void reduce(NullWritable key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {/**
             * 实现reduce处理的逻辑
             */
        }
    }
​
​
}

5、自定义一个Output

  • 需求:将给定的数据按照差评和好评拆分到不同的文件中
  • 自定义分区能不能实现?
    • 可以
  • 自定义OutputFormat
package bigdata.hanjiaxiaozhi.cn.mr.output;import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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;
​
​
/**
 * @ClassName MRUserOutputFormat
 * @Description TODO 自定义一个输出的类
 * @Date 2020/6/2 16:16
 * @Create By     hanjiaxiaozhi
 */
public class MRUserOutputFormat extends FileOutputFormat<Text, NullWritable> {/**
     * 返回一个输出器对象
     * @param context
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext context) throws IOException, InterruptedException {
        //构建HDFS对象
        FileSystem hdfs = FileSystem.get(context.getConfiguration());
        //构建两个输出流
        FSDataOutputStream badContent = hdfs.create(new Path("datas/output/badcontent/bad.txt"));
        FSDataOutputStream goodContent = hdfs.create(new Path("datas/output/goodcontent/good.txt"));
        //构建输出器对象
        MrUserRecordWriter mrUserRecordWriter = new MrUserRecordWriter(badContent,goodContent);
        return mrUserRecordWriter;
    }
}

  • 自定义一个输出器:RecordWrite
package bigdata.hanjiaxiaozhi.cn.mr.output;import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;import java.io.IOException;/**
 * @ClassName MrUserRecordWriter
 * @Description TODO 负责真正实现将数据进行存储
 * @Date 2020/6/2 16:17
 * @Create By     hanjiaxiaozhi
 */
public class MrUserRecordWriter extends RecordWriter<Text, NullWritable> {
    FSDataOutputStream badContent = null;
    FSDataOutputStream goodContent = null;public MrUserRecordWriter(FSDataOutputStream badContent, FSDataOutputStream goodContent) {
        this.badContent = badContent;
        this.goodContent = goodContent;
    }/**
     * 真正负责将每条keyvalue输出的方法
     * 只需要在这个方法中构建输出的API,将每条KeyValue输出即可
     * @param key
     * @param value
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    public void write(Text key, NullWritable value) throws IOException, InterruptedException {
        //获取数据中的评价
        String content = key.toString().split("\t")[9];
        //如果是差评,就写入一个文件
        if("2".equals(content)){
            badContent.write(key.toString().getBytes());
            badContent.write("\r\n".getBytes());//添加换行
        }else {
            //如果不是差评,写入另外一个文件
            goodContent.write(key.toString().getBytes());
            goodContent.write("\r\n".getBytes());
        }
    }/**
     * 释放资源的方法,最后执行的方法
     * @param context
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    public void close(TaskAttemptContext context) throws IOException, InterruptedException {
        badContent.close();
        goodContent.close();
    }
}
  • MR实现
package bigdata.hanjiaxiaozhi.cn.mr.output;import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
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.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;import java.io.IOException;/**
 * @ClassName MRDriver
 * @Description TODO 实现用户自定义输出
 * @Date 2020/5/30 10:34
 * @Create By     hanjiaxiaozhi
 */
public class MRUserOutput extends Configured implements Tool {/**
     * 用于将Job的代码封装
     * @param args
     * @return
     * @throws Exception
     */
    @Override
    public int run(String[] args) throws Exception {
        //todo:1-构建一个Job
        Job job = Job.getInstance(this.getConf(),"model");//构建Job对象,调用父类的getconf获取属性的配置
        job.setJarByClass(MRUserOutput.class);//指定可以运行的类型
        //todo:2-配置这个Job
        //input
//        job.setInputFormatClass(TextInputFormat.class);//设置输入的类的类型,默认就是TextInputFormat
        Path inputPath = new Path("datas/outputformat/ordercomment.csv");//用程序的第一个参数做为第一个输入路径
        //设置的路径可以给目录,也可以给定文件,如果给定目录,会将目录中所有文件作为输入,但是目录中不能包含子目录
        TextInputFormat.setInputPaths(job,inputPath);//为当前job设置输入的路径//map
        job.setMapperClass(MRMapper.class);//设置Mapper的类,需要调用对应的map方法
        job.setMapOutputKeyClass(Text.class);//设置Mapper输出的key类型
        job.setMapOutputValueClass(NullWritable.class);//设置Mapper输出的value类型//shuffle
//        job.setPartitionerClass(HashPartitioner.class);//自定义分区
//        job.setGroupingComparatorClass(null);//自定义分组的方式
//        job.setSortComparatorClass(null);//自定义排序的方式//reduce
        job.setReducerClass(MRReducer.class);//设置Reduce的类,需要调用对应的reduce方法
        job.setOutputKeyClass(Text.class);//设置Reduce输出的Key类型
        job.setOutputValueClass(NullWritable.class);//设置Reduce输出的Value类型
        job.setNumReduceTasks(1);//设置ReduceTask的个数,默认为1//output:输出目录默认不能提前存在
        job.setOutputFormatClass(MRUserOutputFormat.class);//设置输出的类,默认我诶TextOutputFormat
        Path outputPath = new Path("datas/output/outputformat");//用程序的第三个参数作为输出
        //解决输出目录提前存在,不能运行的问题,提前将目前删掉
        //构建一个HDFS的文件系统
        FileSystem hdfs = FileSystem.get(this.getConf());
        //判断输出目录是否存在,如果存在就删除
        if(hdfs.exists(outputPath)){
            hdfs.delete(outputPath,true);
        }
        MRUserOutputFormat.setOutputPath(job,outputPath);//为当前Job设置输出的路径//todo:3-提交运行Job
        return job.waitForCompletion(true) ? 0:-1;
    }/**
     * 程序的入口,调用run方法
     * @param args
     */
    public static void main(String[] args) throws Exception {
        //构建一个Configuration对象,用于管理这个程序所有配置,工作会定义很多自己的配置
        Configuration conf = new Configuration();
        //t通过Toolruner的run方法调用当前类的run方法
        int status = ToolRunner.run(conf, new MRUserOutput(), args);
        //退出程序
        System.exit(status);
    }
​
​
    /**
     * @ClassName MRMapper
     * @Description TODO 这是MapReduce模板的Map类
     *      输入的KV类型:由inputformat决定,默认是TextInputFormat
     *      输出的KV类型:由map方法中谁作为key,谁作为Value决定
     */
    public static class MRMapper extends Mapper<LongWritable, Text, Text,NullWritable> {
        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            //将每一行的每条数据作为Key
            context.write(value,NullWritable.get());
        }
    }
​
​
​
    /**
     * @ClassName MRReducer
     * @Description TODO MapReduce模板的Reducer的类
     *      输入的KV类型:由Map的输出决定,保持一致
     *      输出的KV类型:由reduce方法中谁作为key,谁作为Value决定
     */
    public static class MRReducer extends Reducer<Text,NullWritable,Text,NullWritable> {
        @Override
        protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
            context.write(key,NullWritable.get());
        }
    }
​
​
}
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页