自定义分组及TopN的实现
1、分组:默认分组按照整个Key进行分组,Key相同,就是同一组
- 本质:类似于排序,也是一种比较
如果相同就是同一组,如果不同就不是一组
- 自定义分组比较器的规则
Class extends WritableComparator
- 重写compare方法
- 如果返回0,就是同一组
- 如果返回非0,就不是一组
2、需求
- 数据
订单id 商品id 商品价格
Order_0000001 Pdt_01 222.8
Order_0000001 Pdt_05 25.8
Order_0000002 Pdt_03 522.8
Order_0000002 Pdt_04 122.4
Order_0000002 Pdt_05 722.4
Order_0000003 Pdt_01 222.8
- 需求
- 统计得到每个订单价格最高的商品信息
3、分析
Step1:分析结果
Order_0000001 Pdt_01 222.8
Order_0000002 Pdt_05 722.4
Order_0000003 Pdt_01 222.8
只保留每个订单中商品价格最高的那条
Step2:有没有分组或者排序,用于决定Map输出的Key是什么
- 分组:每个关键字
- 要按照订单id分组
- 排序:最高
- 要按照价格排序
- 问题来了:到底谁作为Key?
- 两个需求都有,而且不是同一字段,怎么办?
- 将整个数据三列作为一个整体,共同作为key,自定义数据类型
- 实现比较器排序方法:如果订单相同,就按照价格降序排序
Step3:Value:NullWritable
Step4:
- Input:读取这个文件
- Map
- 将这个文件的每一行的三列构建成一个 JavaBean
- 用三列作为Key,Null作为Value
- Shuffle
- 分区:默认按照key进行分区,相同key进入同一个分区
- 问题来了:当前的key是三个字段的整体,如果有多个分区,按照默认分区,相同的订单可能会进入不同的分区不同的Reduce中
- 如果使用默认排序,相同订单的数据进入了不同的Reduce,无法实现按照订单分组,取每个订单最高的价格
- 必须要让相同的订单进入同一个Reduce,按照订单id分组,取最大的那个价格
- 不能使用默认分区,需要自定义分区,不能按照整个key进行分区,要按照key里面的订单id进行分区,只要订单id相同 ,就进入同一个分区
- 排序:自定义数据类,封装三列,作为一个整体
- 根据需求,如果订单id相同,按照价格降序,才能取到最高的价格
- 如果订单id不相同,按照订单id排序
- 分组:默认按照key进行分组,key相同作为同一组
- key:三个字段的整体,每一条数据都不一样,每一条数据都是一组
- 如果每一条数据都是一组,怎么取组内最高的那条?
- 需要:自定义分组,将相同订单id的数据放入同一组,根据排序得到最高的那条
- 分区:默认按照key进行分区,相同key进入同一个分区
- Reduce
- 分组以后,每个订单的第一条就是最高价格 的那一条,直接输出即可
- Output
4、代码实现
自定义数据类型
package bigdata.hanjiaxiaozhi.cn.mr.orders;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* @ClassName UserOrderBean
* @Description TODO 自定义数据类型,用于封装订单id、商品id、商品price
* @Date 2020/6/2 11:40
* @Create By hanjiaxiaozhi
*/
public class UserOrderBean implements WritableComparable<UserOrderBean> {
private String orderId;
private String productId;
private double price;
public UserOrderBean(){
}
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
@Override
public String toString() {
return orderId+"\t"+productId+"\t"+price;
}
/**
* 先比较订单id,订单id不相同,直接默认升序
* 如果订单id相同,就比较价格,降序
* @param o
* @return
*/
@Override
public int compareTo(UserOrderBean o) {
//先比较订单是否相同
int comp = this.getOrderId().compareTo(o.getOrderId());
//如果订单id相同
if(comp == 0){
//相同订单id按照价格降序排序
return -Double.valueOf(this.getPrice()).compareTo(Double.valueOf(o.getPrice()));
}
//如果订单id不相同,按照订单id升序排序
return comp;
}
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(this.orderId);
out.writeUTF(this.productId);
out.writeDouble(this.price);
}
@Override
public void readFields(DataInput in) throws IOException {
this.orderId = in.readUTF();
this.productId = in.readUTF();
this.price = in.readDouble();
}
}
自定义分区
package bigdata.hanjiaxiaozhi.cn.mr.orders;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Partitioner;
/**
* @ClassName UserOrderPartition
* @Description TODO 自定义分区,让相同订单的进入同一个分区
* @Date 2020/6/2 11:49
* @Create By hanjiaxiaozhi
*/
public class UserOrderPartition extends Partitioner<UserOrderBean, NullWritable> {
@Override
public int getPartition(UserOrderBean key, NullWritable value, int numPartitions) {
//用key中的订单id作为分区的条件
return (key.getOrderId().hashCode() & Integer.MAX_VALUE) % numPartitions;
}
}
自定义分组
package bigdata.hanjiaxiaozhi.cn.mr.orders;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;
/**
* @ClassName UserOrderGroup
* @Description TODO 自定义分组比较器,按照订单id进行分组
* @Date 2020/6/2 11:52
* @Create By Frank
*/
public class UserOrderGroup extends WritableComparator {
//将Key的类型进行注册
public UserOrderGroup(){
super(UserOrderBean.class,true);
}
@Override
public int compare(WritableComparable a, WritableComparable b) {
UserOrderBean o1 = (UserOrderBean) a;
UserOrderBean o2 = (UserOrderBean) b;
//比较订单id是否相等,相等就是同一组
return o1.getOrderId().compareTo(o2.getOrderId());
}
}
Mr
package bigdata.hanjiaxiaozhi.cn.mr.orders;
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.mapreduce.lib.partition.HashPartitioner;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
import java.io.IOException;
/**
* @ClassName MRDriver
* @Description TODO 这是MapReduce程序的Driver类的模板
* @Date 2020/5/30 10:34
* @Create By hanjiaxiaozhi
*/
public class MROrdersTop1 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(),"top1");//构建Job对象,调用父类的getconf获取属性的配置
job.setJarByClass(MROrdersTop1.class);//指定可以运行的类型
//todo:2-配置这个Job
//input
// job.setInputFormatClass(TextInputFormat.class);//设置输入的类的类型,默认就是TextInputFormat
Path inputPath = new Path("datas/orders/orders.txt");//用程序的第一个参数做为第一个输入路径
//设置的路径可以给目录,也可以给定文件,如果给定目录,会将目录中所有文件作为输入,但是目录中不能包含子目录
TextInputFormat.setInputPaths(job,inputPath);//为当前job设置输入的路径
//map
job.setMapperClass(MRMapper.class);//设置Mapper的类,需要调用对应的map方法
job.setMapOutputKeyClass(UserOrderBean.class);//设置Mapper输出的key类型
job.setMapOutputValueClass(NullWritable.class);//设置Mapper输出的value类型
//shuffle
job.setPartitionerClass(UserOrderPartition.class);//自定义分区
job.setGroupingComparatorClass(UserOrderGroup.class);//自定义分组的方式
// job.setSortComparatorClass(null);//自定义排序的方式
//reduce
job.setReducerClass(MRReducer.class);//设置Reduce的类,需要调用对应的reduce方法
job.setOutputKeyClass(UserOrderBean.class);//设置Reduce输出的Key类型
job.setOutputValueClass(NullWritable.class);//设置Reduce输出的Value类型
job.setNumReduceTasks(1);//设置ReduceTask的个数,默认为1
//output:输出目录默认不能提前存在
// job.setOutputFormatClass(TextOutputFormat.class);//设置输出的类,默认我诶TextOutputFormat
Path outputPath = new Path("datas/output/orders/output");//用程序的第三个参数作为输出
//解决输出目录提前存在,不能运行的问题,提前将目前删掉
//构建一个HDFS的文件系统
FileSystem hdfs = FileSystem.get(this.getConf());
//判断输出目录是否存在,如果存在就删除
if(hdfs.exists(outputPath)){
hdfs.delete(outputPath,true);
}
TextOutputFormat.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 MROrdersTop1(), 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, UserOrderBean,NullWritable> {
private UserOrderBean outputKey = new UserOrderBean();
private NullWritable outputValue = NullWritable.get();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//将这个文件中的每一行的三列作为key
String[] items = value.toString().split("\t");
//对key进行赋值
this.outputKey.setOrderId(items[0]);
this.outputKey.setProductId(items[1]);
this.outputKey.setPrice(Double.parseDouble(items[2]));
//输出
context.write(this.outputKey,this.outputValue);
}
}
/**
* @ClassName MRReducer
* @Description TODO MapReduce模板的Reducer的类
* 输入的KV类型:由Map的输出决定,保持一致
* 输出的KV类型:由reduce方法中谁作为key,谁作为Value决定
*/
public static class MRReducer extends Reducer<UserOrderBean,NullWritable,UserOrderBean,NullWritable> {
@Override
protected void reduce(UserOrderBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
//直接输出第一条
// context.write(key,NullWritable.get());
//迭代输出每一条
for (NullWritable value : values) {
context.write(key,value);
}
}
}
}