#博学谷IT学习技术支持#
案例:
现有美国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();
}
}
在JavaBean定义中,我们实现的排序比较规则是先按照州名排序,然后再按照确诊病例数排序。可能会有疑惑的地方是为什么需要先按照州名进行排序,而不是直接按照确诊病例数进行排序呢?
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编程模型中,我们需要按照某字段进行分组,必须首先按照该字段进行排序。