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查看,发现有两个输出文件
下载后发现p001、p002、p003在00000文件中:
p004在00001文件中:
至此,就介绍完了分区和规约的案例实战。
感谢观看,如果您觉得文章写得还不错,不妨点个赞。如果您觉得有什么疑惑或者不对的地方,可以留下评论,看到我会及时回复的。如果您关注一下我,那么我会更高兴的,谢谢!