从Top N 问题窥探MapReduce分组前排序思想

#博学谷IT学习技术支持#

关于TopN 问题中的排序

  案例:
  现有美国2021-1-28号,各个县county的新冠疫情累计案例信息,包括确诊病例和死亡病例,数据格式如下所示:

2021-01-28,Juneau City and Borough,Alaska,02110,1108,3
2021-01-28,Kenai Peninsula Borough,Alaska,02122,3866,18
2021-01-28,Ketchikan Gateway Borough,Alaska,02130,272,1
2021-01-28,Kodiak Island Borough,Alaska,02150,1021,5
2021-01-28,Kusilvak Census Area,Alaska,02158,1099,3
2021-01-28,Lake and Peninsula Borough,Alaska,02164,5,0
2021-01-28,Matanuska-Susitna Borough,Alaska,02170,7406,27
2021-01-28,Nome Census Area,Alaska,02180,307,0
2021-01-28,North Slope Borough,Alaska,02185,973,3
2021-01-28,Northwest Arctic Borough,Alaska,02188,567,1
2021-01-28,Petersburg Borough,Alaska,02195,43,0
字段含义如下:date(日期),county(县),state(州),fips(县编码code),cases(累计确诊病例),deaths(累计死亡病例)。
需求:找出美国2021-01-28,每个州state的确诊案例数最多的县county前3个。Top3问题。

上面的问题中,我们使用MapReduce编码实现,其思路是需要按照州名进行分组,然后每个州内按照确诊病例数进行倒序排序。因此需要定义一个JavaBean对象,实现WriteComparable接口,重写compareTo方法;同时需要自定义分组规则,将州名相同的数据分到同一组。
Mapper类代码如下:

public class CovidTopMapper extends
        Mapper<LongWritable, Text, CovidBean, NullWritable> {

    @Override
    protected void map(LongWritable key, Text value, Context context)
            throws IOException, InterruptedException {
        String[] splitArr = value.toString().split(",");
        CovidBean covidBean = new CovidBean();
        covidBean.setDate(splitArr[0]);
        covidBean.setCounty(splitArr[1]);
        covidBean.setState(splitArr[2]);
        covidBean.setCode(splitArr[3]);
        covidBean.setCases(Integer.valueOf(splitArr[4]));
        covidBean.setDeaths(splitArr.length > 5 ? Integer.valueOf(splitArr[5]) : 0);
        context.write(covidBean, NullWritable.get());
    }
}

Reducer类代码如下:

public class CovidTopReducer
        extends Reducer<CovidBean, NullWritable, CovidBean, NullWritable> {

    @Override
    protected void reduce(CovidBean key, Iterable<NullWritable> values, Context context)
            throws IOException, InterruptedException {
        int i = 0;
        for (NullWritable value : values) {
            if (i > 2) {
                break;
            }
            context.write(key, value);
            i++;
        }

    }
}

JavaBean定义:

@Data
public class CovidBean implements WritableComparable<CovidBean> {

    private String date;
    private String county;
    private String state;
    private String code;
    private int cases;
    private int deaths;



    @Override
    public void write(DataOutput out) throws IOException {
        out.writeUTF(date);
        out.writeUTF(county);
        out.writeUTF(state);
        out.writeUTF(code);
        out.writeInt(cases);
        out.writeInt(deaths);
    }

    @Override
    public void readFields(DataInput in) throws IOException {
        this.date = in.readUTF();
        this.county = in.readUTF();
        this.state = in.readUTF();
        this.code = in.readUTF();
        this.cases = in.readInt();
        this.deaths = in.readInt();
    }

    @Override
    public String toString() {
        return  county + '\t'
                + state + '\t'
                + cases + '\t'
                + deaths;
    }

    @Override
    public int compareTo(CovidBean o) {
        return this.getState().compareTo(o.getState()) == 0 ?
                -(this.getCases() - o.getCases()) : this.getState().compareTo(o.getState());
//        return o.getCases() - this.getCases();
    }
}

执行结果:
从Top N 问题窥探MapReduce分组前排序思想_第1张图片

在JavaBean定义中,我们实现的排序比较规则是先按照州名排序,然后再按照确诊病例数排序。可能会有疑惑的地方是为什么需要先按照州名进行排序,而不是直接按照确诊病例数进行排序呢?
MapReduce中规定分组字段,必须是排序字段,必须保证分组前数据是有序的,这样才能保证指定分组规则的数据分到同一组。假设我们只按照确诊病例数进行排序,而不首先按照州名进行排序。我们得到的结果是没有分组的结果,是按照确诊病例数进行全局排序的结果,如下:

从Top N 问题窥探MapReduce分组前排序思想_第2张图片
发现和我们预期的结果不同,那么是什么原因导致了这种结果呢,接下来我们就通过分析MapReduce源码,来探究MapReduce分组前排序思想的实现。

MapReduce源码分析分组前排序原理

在TopN案例中,使用了自定义分组比较器,重写了分组比较的逻辑。
从MapReduce执行过程来看,首先是执行Map任务,然后经过Shuffle阶段的分区、排序、分组,最后执行Reduce任务。查看Reducer类源码,在执行reduce方法之前,首先会按照Key对数据进行分组,如下所示:

 public void run(Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
        this.setup(context);

        try {
            while(context.nextKey()) {
                this.reduce(context.getCurrentKey(), context.getValues(), context);
                Iterator<VALUEIN> iter = context.getValues().iterator();
                if (iter instanceof ValueIterator) {
                    ((ValueIterator)iter).resetBackupStore();
                }
            }
        } finally {
            this.cleanup(context);
        }

    }

通过进一步查看context.nextKey的实现,我们发现,迭代器在遍历每一对key和value时,会将当前key和下一个key进行比较,如果相同的话,则会循环遍历,直到下一个key值不相同,退出循环,然后执行reduce方法,在reduce方法中就取到了所有key划分为同一组的值。在这种情况下,始终是将当前值和下一个值进行比较,因此,如果数据没有按照分组的字段进行有序的排列,那么nextKeyIsSame的值为false,循环退出,执行reduce方法,从而导致本应该划分为同一组的key、value键值对被打散,无法实现分组的效果。

  public boolean nextKey() throws IOException, InterruptedException {
        while(this.hasMore && this.nextKeyIsSame) {
            this.nextKeyValue();
        }

        if (this.hasMore) {
            if (this.inputKeyCounter != null) {
                this.inputKeyCounter.increment(1L);
            }

            return this.nextKeyValue();
        } else {
            return false;
        }
    }
    public boolean nextKeyValue() throws IOException, InterruptedException {
        if (!this.hasMore) {
            this.key = null;
            this.value = null;
            return false;
        } else {
            this.firstValue = !this.nextKeyIsSame;
            DataInputBuffer nextKey = this.input.getKey();
            this.currentRawKey.set(nextKey.getData(), nextKey.getPosition(), nextKey.getLength() - nextKey.getPosition());
            this.buffer.reset(this.currentRawKey.getBytes(), 0, this.currentRawKey.getLength());
            this.key = this.keyDeserializer.deserialize(this.key);
            DataInputBuffer nextVal = this.input.getValue();
            this.buffer.reset(nextVal.getData(), nextVal.getPosition(), nextVal.getLength() - nextVal.getPosition());
            this.value = this.valueDeserializer.deserialize(this.value);
            this.currentKeyLength = nextKey.getLength() - nextKey.getPosition();
            this.currentValueLength = nextVal.getLength() - nextVal.getPosition();
            if (this.isMarked) {
                this.backupStore.write(nextKey, nextVal);
            }

            this.hasMore = this.input.next();
            if (this.hasMore) {
                nextKey = this.input.getKey();
                this.nextKeyIsSame = this.comparator.compare(this.currentRawKey.getBytes(), 0, this.currentRawKey.getLength(), nextKey.getData(), nextKey.getPosition(), nextKey.getLength() - nextKey.getPosition()) == 0;
            } else {
                this.nextKeyIsSame = false;
            }

            this.inputValueCounter.increment(1L);
            return true;
        }
    }

以案例中的TopN问题为例,我们希望同一个州的数据分到同一组中,于是我们自定义了分组比较器,比较不同Key的州名是否相等。仅仅定义了分组比较规则就够了吗?从实验的结果来看,我们还必须保证同一个州的数据在相邻的位置上,这样我们nextKeyIsSame的值才能为true,保证相同州的数据分到同一组中,执行同一次reduce方法。为了保证同一个州的数据在相邻位置,那么就必须按照州名进行排序。而为了在同一组中取到TopN的数据,我们必须按照确诊病例对同一州的数据进行排序。
因此,在MapReduce编程模型中,我们需要按照某字段进行分组,必须首先按照该字段进行排序。

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