Hadoop入门指南之分区、规约实战

Hadoop系列文章索引

Hadoop入门指南之HDFS介绍

Hadoop入门指南之Linux环境搭建

Hadoop入门指南之Linux软件安装

Hadoop入门指南之Hadoop安装

Hadoop入门指南之hdfs命令行使用.

Hadoop入门指南之MapReduce介绍

Hadoop入门指南之统计库存实战

Hadoop入门指南之分区、规约实战

Hadoop入门指南之排序实战

Hadoop入门指南之分组实战

Hadoop入门指南之表连接操作

Hadoop入门指南之yarn介绍​​​​​​​

上一篇通过统计库存实战来展示了Map和Reduce阶段,现在来介绍Shuffle阶段的分区和规约。

分区是指根据一定的规则,把数据分成若干个区,分别给不同的Reducer进行处理,最后输出时,相同区的结果会在一个输出文件中,比如分了3个区,最后就会有3个输出文件。

规约英文叫Combiner,我不太明白为什么中文翻译成了规约这个拗口的名称,也不易理解。我的理解就是合并,把相同的key的value合并成一个数据,让Reducer处理。因为Map完的数据在经过Shuffle阶段后,是通过网络来传输给Reducer进行处理的,如果不进行合并,那么就会有大量数据通过网络传输给Reducer,就会导致整个流程变慢。如果在Shuffle阶段做了合并,那么就大大减少了数据的传输量,减轻了网络的传输压力,提升了处理速度。

下面就用一个案例来讲解具体怎么用Java代码实现分区和规约。

还是库存统计的例子,这次我们让数据变得复杂一些

p004,2021-01-01,5,2
p003,2021-01-01,8,1
p002,2021-01-03,3,3
p003,2021-01-03,6,2
p004,2021-01-05,9,1
p001,2021-01-05,3,2
p004,2021-01-05,2,2
p003,2021-01-07,3,1
p002,2021-01-07,6,5
p001,2021-01-08,2,1
p001,2021-01-09,6,1

这次的数据多了一列,代表的是出库的数量,比如第一行就代表p005这个商品在2021年1月1日进了5件,卖出2件。这时候我们发现在统计库存的时候,并不单单是要把入库数累加了,在累加前还要减去出库数。那么K2,K3还是商品id,Text类型,V3还是库存数,IntWritable类型,但是V2成了两对数据,入库数和出库数,这时候最合适的方法应该是定义一个Java对象,它有两个成员变量,入库数和出库数。

再增加两个需求,一是要对数据进行分区,以p003为分界线,id小于等于p003的输出到一个文件中,大于p003的输出到另一个文件中,二是要求数据传输量尽可能的小。

这时候就需要时候到Shuffle阶段的分区一级规约了。分区完成第一个需求,规约完成第二个需求。

先把数据保存为stock.txt,然后rz -E上传到node01,之后使用put命令上传到hdfs中:

hdfs dfs -mkdir /stock_count2_input
hdfs dfs -put stock.txt /stock_count2_input

在上次的项目中新建一个包com.demo.stock_count2。

先实现定义好存放入库数和出库数的Java对象:

package com.demo.stock_count2;

import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableComparable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

public class StockBean implements Writable {
    private Integer inStock;//入库数
    private Integer outStock;//出库数
    public Integer getInStock() {
        return inStock;
    }

    public void setInStock(Integer inStock) {
        this.inStock = inStock;
    }

    public Integer getOutStock() {
        return outStock;
    }

    public void setOutStock(Integer outStock) {
        this.outStock = outStock;
    }

    /**
     * 无参构造方法
     */
    public StockBean() {
    }

    /**
     * 有参构造方法
     * @param inStock 入库数
     * @param outStock 出库数
     */
    public StockBean(Integer inStock, Integer outStock) {
        this.inStock = inStock;
        this.outStock = outStock;
    }

    /**
     * 对象转String的方法
     * @return
     */
    @Override
    public String toString() {
        return inStock+"\t"+outStock;
    }

    /**
     * 序列化
     * @param dataOutput
     * @throws IOException
     */
    @Override
    public void write(DataOutput dataOutput) throws IOException {
        dataOutput.writeInt(inStock);
        dataOutput.writeInt(outStock);
    }

    /**
     * 反序列化
     * @param dataInput
     * @throws IOException
     */
    @Override
    public void readFields(DataInput dataInput) throws IOException {
        inStock = dataInput.readInt();
        outStock = dataInput.readInt();
    }
}

 

这里我们实现了Writable接口,增加了入库数和出库数的成员变量,生成了getter和setter,重写了toString方法,重写序列化和反序列化方法,write和readFields,实现了无参构造和有参构造方法。

下面写Mapper类:

package com.demo.stock_count2;

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

public class StockMapper extends Mapper {
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        String[] strings = value.toString().split(",");//对文本进行拆分
        context.write(new Text(strings[0]),new StockBean(Integer.parseInt(strings[2]),Integer.parseInt(strings[3])));
    }
}

V2是上面写的JavaBean,所以需要使用有参构造方法new一个StockBean对象,然后调用context.write方法写入。

下面写Reducer类:

package com.demo.stock_count2;

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

public class StockReducer extends Reducer {
    @Override
    protected void reduce(Text key, Iterable values, Context context) throws IOException, InterruptedException {
        Integer sum = 0;
        for (StockBean bean : values) {
            sum += bean.getInStock()-bean.getOutStock();//净库存累加
        }
        context.write(key,new IntWritable(sum));
    }
}

这里接收到同一个商品id的StockBean集合,遍历他们,用入库数减去出库数,并累加,就可以获得该商品的净库存总数。

下面需要写Combiner类:

package com.demo.stock_count2;

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

public class StockCombiner extends Reducer {
    @Override
    protected void reduce(Text key, Iterable values, Context context) throws IOException, InterruptedException {
        Integer inStock = 0;
        Integer outStock = 0;
        for (StockBean bean : values) {
            inStock+= bean.getInStock();//入库数累加
            outStock+=bean.getOutStock();//出库数累加
        }
        context.write(key,new StockBean(inStock,outStock));
    }
}

Combiner实际上是一种特殊的Reducer类,只不过在框架运行中,发生在Mapper之后,Reducer之前的Shuffle阶段,要做的就是合并同一个商品Id的StockBean对象,遍历他们,并分别累加入库数和出库数,来获得一个新的StockBean,他是总数。

这样就实现了节省Mapper到Reducer之间的数据传输量的目的。

下面写Partitioner类:

package com.demo.stock_count2;

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;

public class StockPartitioner extends Partitioner {
    private final static String SPLIT_PID="p003";

    /**
     * 定义分区规则
     * 商品id小于等于p003为一区,大于p003为二区
     * @param text 商品id
     * @param stockBean 库存Java对象
     * @param i 分区数
     * @return
     */
    @Override
    public int getPartition(Text text, StockBean stockBean, int i) {
        String pid = text.toString();
        if(pid.compareTo(SPLIT_PID)<=0){
            return 0;
        }
        else{
            return 1;
        }
    }
}

Partitioner就是实现分区的类,他的Key和Value就是K2、V2,先定义分界线p003,然后拿key,也就是商品id和分界线对比,小于等于分界线的返回0,大于分界线的返回1,这里返回的数字就代表的是分区编号,和数组一样,从0开始。

最后写主类:

package com.demo.stock_count2;

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.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
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.net.URI;

public class StockMain extends Configured implements Tool {
    @Override
    public int run(String[] strings) throws Exception {
        Job job = Job.getInstance(super.getConf(), "stock_count2");
        job.setJarByClass(StockMain.class);//指定jar的主类
        job.setInputFormatClass(TextInputFormat.class);//指定输入类
        TextInputFormat.addInputPath(job,new Path("hdfs://node01:8020/stock_count2_input"));//指定输入路径
        job.setMapperClass(StockMapper.class);//指定Mapper类
        job.setMapOutputKeyClass(Text.class);//指定K2
        job.setMapOutputValueClass(StockBean.class);//指定V2
        job.setPartitionerClass(StockPartitioner.class);//指定分区类
        job.setNumReduceTasks(2);//指定分区数
        job.setCombinerClass(StockCombiner.class);//指定规约类
        job.setReducerClass(StockReducer.class);//指定Reducer类
        job.setOutputKeyClass(Text.class);//指定K3
        job.setOutputValueClass(IntWritable.class);//指定V3
        job.setOutputFormatClass(TextOutputFormat.class);//指定输出类
        Path path = new Path("hdfs://node01:8020/stock_count2_output");
        FileSystem fileSystem = FileSystem.get(new URI("hdfs://node01:8020"), new Configuration());
        boolean exists = fileSystem.exists(path);
        if(exists){
            fileSystem.delete(path,true);//如果目录存在,就先删除目录
        }
        TextOutputFormat.setOutputPath(job,path);//设置输出路径
        boolean b = job.waitForCompletion(true);//运行job
        fileSystem.close();//关闭文件系统
        return b?0:1;
    }

    public static void main(String[] args) throws Exception {
        Configuration configuration = new Configuration();//新建一个配置对象
        int run = ToolRunner.run(configuration, new StockMain(), args);//运行MapReduce
        System.exit(run);//系统退出
    }
}

和以前的区别就在于指定了分区类,设置了分区数,指定了规约类。

打成jar包,放到node01上,然后运行:

 hadoop jar original-mapreduce_demo-1.0-SNAPSHOT.jar com.demo.stock_count2.StockMain

然后到http://node01:50070/explorer.html#/stock_count2_output查看,发现有两个输出文件

Hadoop入门指南之分区、规约实战_第1张图片

下载后发现p001、p002、p003在00000文件中:

p004在00001文件中:

至此,就介绍完了分区和规约的案例实战。

感谢观看,如果您觉得文章写得还不错,不妨点个赞。如果您觉得有什么疑惑或者不对的地方,可以留下评论,看到我会及时回复的。如果您关注一下我,那么我会更高兴的,谢谢!

你可能感兴趣的:(大数据,hadoop)