hadoop mapreduce求解有序TopN

利用hadoop的map和reduce排序特性实现对数据排序取TopN条数据。

代码参考:https://github.com/asker124143222/wordcount

1、样本数据,假设是订单数据,求解按订单id排序且每个订单里价格最高前三,从高到低排序。

订单ID  商品ID   单价
0000001	Pdt_01	222.8
0000002	Pdt_05	722.4
0000001	Pdt_02	33.8
0000003	Pdt_06	232.8
0000003	Pdt_02	33.8
0000002	Pdt_03	522.8
0000002	Pdt_04	122.4
0000001	Pdt_01	122.8
0000002	Pdt_05	522.4
0000003	Pdt_02	133.8
0000002	Pdt_03	222.8
0000002	Pdt_04	222.4
0000001	Pdt_01	322.8
0000002	Pdt_05	322.4

2、求解思路

1.将订单封装成bean,以bean对象作为map的key,这样才能利用hadoop的key自动排序特性。
2.实现WritableComparable接口,bean以id升序,价格降序实现比较接口,这样数据在map后进入shuffle阶段会实现自定义规则自排序。
3、reduce阶段,如果不做任何处理数据将呈现将按订单升序,价格降序。但是订单id相同,价格不同的订单将不能使用同一个reduce函数,也不能求解topN(是指利用key排序特性的topN,否则实现topN的手段还有很多)。
4、使用reduce阶段分组特性接口WritableComparator,在reduce归并前,将对数据进行分组,以决定什么样的数据进入同一分组里,即同一个reduce里。
5、在实现WritableComparator的类中,以bean为基础,我们将订单id作为比较项忽略价格因素,实现同一id,进入同一个分组,价格从高到低已经在bean里排序实现,shuffle阶段也遵循了这个原则,所以在reduce阶段不考虑价格排序问题。
6、最后一个难点,通过给reduce的数据分组,传递到reduce里key就是同一个订单id最大价格的订单,一般情况下,我们从map阶段传递过来的values都是null,reduce阶段也是一个null值的迭代器,如何求topN呢,这个时候实现的只是max。
7、这个才是最后一个步骤,null的迭代器里其实存了分组里每一个值,包括key和value(虽然value是null),而这个迭代器里key和reduce的key是共享地址,也就是指向同一个变量,
当我们使用迭代器滚动取值的时候,reduce里的key的值也被迭代里的key值重新赋值(它们指向同一内存地址),所以在执行迭代过程中,我们就可以轻松求解TopN。

3、code

3.1 OrderBean

public class OrderBean implements WritableComparable<OrderBean> {

    private int order_id;
    private double price;

    public OrderBean() {
    }

    public OrderBean(int order_id, double price) {
        this.order_id = order_id;
        this.price = price;
    }

    @Override
    public int compareTo(OrderBean o) {
        //订单id升序,价格降序
        if(this.getOrder_id()>o.getOrder_id()){
            return 1;
        }else if (this.getOrder_id()<o.getOrder_id()){
            return -1;
        }
        else{
            if(this.getPrice()>o.getPrice()){
                return -1;
            }else if(this.getPrice()<o.getPrice()){
                return 1;
            }else{
                return 0;
            }
        }
    }

    @Override
    public void write(DataOutput out) throws IOException {
        out.writeInt(order_id);
        out.writeDouble(price);
    }

    @Override
    public void readFields(DataInput in) throws IOException {
        this.order_id = in.readInt();
        this.price = in.readDouble();
    }

    public int getOrder_id() {
        return order_id;
    }

    public void setOrder_id(int order_id) {
        this.order_id = order_id;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "order_id=" + order_id +
                ", price=" + price;
    }
}

3.2 mapper

public class OrderMapper extends Mapper<LongWritable, Text,OrderBean, NullWritable> {
    OrderBean k = new OrderBean();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        // 1 获取一行
        String line = value.toString();

        // 2 分割
        String[] fields = line.split("\\s+");

        // 3 封装对象
        k.setOrder_id(Integer.parseInt(fields[0]));
        k.setPrice(Double.parseDouble(fields[2]));

        // 4 写出,value值是null
        context.write(k, NullWritable.get());

    }
}

3.3 reducer的分组规则

/**
 * @Author: xu.dm
 * @Date: 2019/8/30 16:15
 * @Version: 1.0
 * @Description: reducer数据分组,在数据从map阶段送到reducer后,在归并执行前,重新进行分组
 * 通过这种方式,重新调整数据进入reducer的key值
 *
 * 本例中,map送过来的key是OrderBean,也是按Orderbean排序(id升序,价格降序),
 * 数据送到reduce后,如果不分组,那么相同id不同价格的数据被认为是不同的key,
 * 经过自定义分组(继承WritableComparator),只使用id作为分组的条件,
 * reduce在归并前key的数据只按id判断,价格被忽略,
 * 那么:{1,300}和{1,200}这种数据就会被认为是相同的key,即key=1,其他忽略,所以最终输出的时候,价格只保留最高。
 **/
public class OrderSortGroupingComparator extends WritableComparator {


    protected OrderSortGroupingComparator() {
        super(OrderBean.class, true);
    }

    @Override
    public int compare(WritableComparable a, WritableComparable b) {
        OrderBean aBean = (OrderBean)a;
        OrderBean bBean = (OrderBean)b;
        if(aBean.getOrder_id()>bBean.getOrder_id()){
            return 1;
        }else if(aBean.getOrder_id()<bBean.getOrder_id()){
            return -1;
        }else {
            return 0;
        }
    }
}

3.4 reducer

/**
 * @Author: xu.dm
 * @Date: 2019/8/30 16:21
 * @Version: 1.0
 * @Description: 如果不执行Iterable<NullWritable> values迭代,直接取key
 * 那么key根据分组只保留从map->shuffle->reduce流程里第一个排序值,如果key是一个bean对象(即复合键),key就是排序输出的第一个对象。
 * 
 * 如果执行Iterable<NullWritable> values迭代,那么迭代器滚动数据过程中,会依次对OrderBean key赋值,
 * 原理是Iterable<NullWritable> values里也存了key值,滚动中key被取出,而迭代器里key和reduce里key公用内存地址(复用)
 * 所以迭代器滚动过程,对key和value都进行了赋值
 * 可以用 OrderBean mykey = new OrderBean(key.getOrder_id(),key.getPrice());来测试,mykey是不会跟着迭代器滚动的。
 * 通过这个特性,可以实现排序数据取TopN
 **/
public class OrderReducer extends Reducer<OrderBean, NullWritable,OrderBean,NullWritable> {
    @Override
    protected void reduce(OrderBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
//        OrderBean mykey = new OrderBean(key.getOrder_id(),key.getPrice());
        //实现topN数据输出
        int topN = 3;
        for (NullWritable value : values) {
            System.out.println(key.hashCode());
            context.write(key,NullWritable.get());
            topN--;
            if(topN<=0)break;
        }


    }
}

3.5 driver

/**
 * @Author: xu.dm
 * @Date: 2019/8/30 16:25
 * @Version: 1.0
 * @Description: 求解每个订单最大单价订单以及取TopN。
 **/
public class OrderDriver {
    public static void main(String[] args) throws Exception, IOException {
        if(args.length!=2)
        {
            System.err.println("使用格式:FlowSortedDriver <input path> <output path>");
            System.exit(-1);
        }


        // 1 获取配置信息
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        // 2 设置jar包加载路径
        job.setJarByClass(OrderDriver.class);

        // 3 加载map/reduce类
        job.setMapperClass(OrderMapper.class);
        job.setReducerClass(OrderReducer.class);

        // 4 设置map输出数据key和value类型
        job.setMapOutputKeyClass(OrderBean.class);
        job.setMapOutputValueClass(NullWritable.class);

        // 5 设置最终输出数据的key和value类型
        job.setOutputKeyClass(OrderBean.class);
        job.setOutputValueClass(NullWritable.class);

        // 6 设置输入数据和输出数据路径
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        // 8 设置reduce端的分组
        job.setGroupingComparatorClass(OrderSortGroupingComparator.class);

        // 7 提交
       //测试环境下,可以先删除目标目录
        Path outPath = new Path(args[1]);
        FileSystem fs = FileSystem.get(conf);
        if(fs.exists(outPath)){
            fs.delete(outPath,true);
        }

        long startTime = System.currentTimeMillis();
        boolean result = job.waitForCompletion(true);

        long endTime = System.currentTimeMillis();
        long timeSpan = endTime - startTime;
        System.out.println("运行耗时:"+timeSpan+"毫秒。");

        System.exit( result ? 0 : 1);
    }

}

3.6 刷出结果

order_id=1, price=322.8
order_id=1, price=222.8
order_id=1, price=122.8
order_id=2, price=722.4
order_id=2, price=522.8
order_id=2, price=522.4
order_id=3, price=232.8
order_id=3, price=133.8
order_id=3, price=33.8

 

 

 
posted @ 2019-08-31 15:08  我是属车的  阅读(499)  评论(0编辑  收藏  举报