MapReduce-join连接
join连接
MapReduce能够执行大型数据集间的连接(join)操作。连接操作的具体实现技术取决于数据集的规模及分区方式
连接操作如果由mapper执行,则称为“map端连接”;如果由reducer执行,则称为“reduce端连接”。
Map端连接
在两个大规模输入数据集之间的map端连接会在数据到达map函数之前就执行连接操作。为达到该目的,各map的输入数据必须先分区并且以特定方式排序。各个输入数据集被划分成相同数量的分区,并且均按相同的键(连接键)排序。同一键的所有记录均会放在同一分区之中。
Map端连接操作可以连接多个作业的输出,只要这些作业的reducer数量相同、键相同并且输出文件是不可切分的(例如,小于一个HDFS块,或gzip压缩)。
Reduce端连接
由于reduce端连接并不要求输入数据集符合特定结构,因而reduce端连接比map端连接更为常用。但是,由于两个数据集均需经过MapReduce的shuffle过程,所以reduce端连接的效率往往要低一些。基本思路是mapper为各个记录标记源,并且使用连接件作为map输出键,使键相同的记录放在同一reducer中。
需要使用以下技术
1.多输入
数据集的输入源往往有多中格式,因此可以使用MultipleInputs类来方便地解析和标注各个源。
2.辅助排序
reducer将从两个源中选出键相同的记录且并不介意这些记录是否已排好序。此外,为了更好的执行连接操作,先将某一个源的数据传输到reducer会非常重要。
举个例子
现有气象站文件及气象数据文件,需要将两个文件进行关联
气象站文件内容如下
00001,北京 00002,天津 00003,山东
气象数据文件内容如下
00001,20180101,15 00001,20180102,16 00002,20180101,25 00002,20180102,26 00003,20180101,35 00003,20180102,36
要求:输出气象站ID 气象站名称及气象数据
代码如下
1.JoinRecordWithStationName类
package com.zhen.mapreduce.join; import java.io.IOException; import java.util.Iterator; import org.apache.hadoop.conf.Configured; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.Mapper; import org.apache.hadoop.mapreduce.Partitioner; import org.apache.hadoop.mapreduce.Reducer; import org.apache.hadoop.mapreduce.lib.input.MultipleInputs; import org.apache.hadoop.mapreduce.lib.input.TextInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import org.apache.hadoop.util.Tool; import org.apache.hadoop.util.ToolRunner; /** * @author FengZhen * @date 2018年9月16日 * */ public class JoinRecordWithStationName extends Configured implements Tool{ /** * 在reduce端连接中,标记气象站记录的mapper * @author FengZhen * 00001,北京 00002,天津 00003,山东 */ static class JoinStationMapper extends Mapper<LongWritable, Text, TextPair, Text>{ private NcdcStationMetadataParser parser = new NcdcStationMetadataParser(); @Override protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, TextPair, Text>.Context context) throws IOException, InterruptedException { if (parser.parse(value.toString())) { context.write(new TextPair(parser.getStationId(), "0"), new Text(parser.getStationName())); } } } /** * 在reduce端连接中标记天气记录的mapper * @author FengZhen * 00001,20180101,15 00001,20180102,16 00002,20180101,25 00002,20180102,26 00003,20180101,35 00003,20180102,36 */ static class JoinRecordMapper extends Mapper<LongWritable, Text, TextPair, Text> { private NcdcRecordParser parser = new NcdcRecordParser(); @Override protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, TextPair, Text>.Context context) throws IOException, InterruptedException { parser.parse(value.toString()); context.write(new TextPair(parser.getStationId(), "1"), value); } } /** * reducer知道自己会先接收气象站记录。因此从中抽取出值,并将其作为后续每条输出记录的一部分写到输出文件。 * @author FengZhen * */ static class JoinReducer extends Reducer<TextPair, Text, Text, Text> { @Override protected void reduce(TextPair key, Iterable<Text> values, Reducer<TextPair, Text, Text, Text>.Context context) throws IOException, InterruptedException { Iterator<Text> iterator = values.iterator(); //取气象站名 Text stationName = new Text(iterator.next()); while (iterator.hasNext()) { Text record = iterator.next(); Text outValue = new Text(stationName.toString() + "\t" + record.toString()); context.write(key.getFirst(), outValue); } } } static class KeyPartitioner extends Partitioner<TextPair, Text>{ @Override public int getPartition(TextPair key, Text value, int numPartitions) { return (key.getFirst().hashCode() & Integer.MAX_VALUE) % numPartitions; } } public int run(String[] args) throws Exception { Job job = Job.getInstance(getConf()); job.setJobName("JoinRecordWithStationName"); job.setJarByClass(JoinRecordWithStationName.class); Path ncdcInputPath = new Path(args[0]); Path stationInputPath = new Path(args[1]); Path outputPath = new Path(args[2]); MultipleInputs.addInputPath(job, ncdcInputPath, TextInputFormat.class, JoinRecordMapper.class); MultipleInputs.addInputPath(job, stationInputPath, TextInputFormat.class, JoinStationMapper.class); FileOutputFormat.setOutputPath(job, outputPath); job.setPartitionerClass(KeyPartitioner.class); job.setGroupingComparatorClass(TextPair.FirstComparator.class); job.setMapOutputKeyClass(TextPair.class); job.setReducerClass(JoinReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(Text.class); return job.waitForCompletion(true) ? 0 : 1; } public static void main(String[] args) { String[] params = new String[] { "hdfs://fz/user/hdfs/MapReduce/data/join/JoinRecordWithStationName/input/record", "hdfs://fz/user/hdfs/MapReduce/data/join/JoinRecordWithStationName/input/station", "hdfs://fz/user/hdfs/MapReduce/data/join/JoinRecordWithStationName/output"}; int exitCode = 0; try { exitCode = ToolRunner.run(new JoinRecordWithStationName(), params); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } System.exit(exitCode); } }
2.NcdcRecordParser类
package com.zhen.mapreduce.join; import java.io.Serializable; /** * @author FengZhen * @date 2018年9月9日 * 解析天气数据 */ public class NcdcRecordParser implements Serializable{ private static final long serialVersionUID = 1L; /** * 气象台ID */ private String stationId; /** * 时间 */ private long timeStamp; /** * 气温 */ private Integer temperature; /** * 解析 * @param value */ public void parse(String value) { String[] values = value.split(","); if (values.length >= 3) { stationId = values[0]; timeStamp = Long.parseLong(values[1]); temperature = Integer.valueOf(values[2]); } } /** * 校验是否合格 * @return */ public boolean isValidTemperature() { return null != temperature; } public String getStationId() { return stationId; } public void setStationId(String stationId) { this.stationId = stationId; } public long getTimeStamp() { return timeStamp; } public void setTimeStamp(long timeStamp) { this.timeStamp = timeStamp; } public Integer getTemperature() { return temperature; } public void setTemperature(Integer temperature) { this.temperature = temperature; } }
3.NcdcStationMetadataParser类
package com.zhen.mapreduce.join; import java.io.Serializable; /** * @author FengZhen * @date 2018年9月9日 * 解析气象台数据 */ public class NcdcStationMetadataParser implements Serializable{ private static final long serialVersionUID = 1L; /** * 气象台ID */ private String stationId; /** * 气象台名称 */ private String stationName; /** * 解析 * @param value */ public boolean parse(String value) { String[] values = value.split(","); if (values.length >= 2) { stationId = values[0]; stationName = values[1]; return true; } return false; } public String getStationId() { return stationId; } public void setStationId(String stationId) { this.stationId = stationId; } public String getStationName() { return stationName; } public void setStationName(String stationName) { this.stationName = stationName; } }
4.TextPair类
package com.zhen.mapreduce.join; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import org.apache.hadoop.io.Text; import org.apache.hadoop.io.WritableComparable; import org.apache.hadoop.io.WritableComparator; /** * @author FengZhen * @date 2018年9月16日 * */ public class TextPair implements WritableComparable<TextPair>{ private Text first; private Text second; public TextPair() { set(new Text(), new Text()); } public TextPair(String first, String second) { set(new Text(first), new Text(second)); } public TextPair(Text first, Text second) { set(first, second); } public void set(Text first, Text second) { this.first = first; this.second = second; } public void write(DataOutput out) throws IOException { first.write(out); second.write(out); } public void readFields(DataInput in) throws IOException { first.readFields(in); second.readFields(in); } @Override public int hashCode() { return first.hashCode() * 163 + second.hashCode(); } @Override public boolean equals(Object obj) { if (obj instanceof TextPair) { TextPair textPair = (TextPair) obj; return first.equals(textPair.first) && second.equals(textPair.second); } return false; } public int compareTo(TextPair o) { int cmp = first.compareTo(o.first); if (cmp != 0) { return cmp; } return second.compareTo(o.second); } public Text getFirst() { return first; } public void setFirst(Text first) { this.first = first; } public Text getSecond() { return second; } public void setSecond(Text second) { this.second = second; } @Override public String toString() { return first + "\t" + second; } /** * 比较两个int值大小 * 降序 * @param a * @param b * @return */ public static int compare(Text a, Text b) { return a.compareTo(b); } static class FirstComparator extends WritableComparator{ protected FirstComparator() { super(TextPair.class, true); } @Override public int compare(WritableComparable a, WritableComparable b) { TextPair ip1 = (TextPair) a; TextPair ip2 = (TextPair) b; return TextPair.compare(ip1.getFirst(), ip2.getFirst()); } } }
打jar包,上传并执行
scp /Users/FengZhen/Desktop/Hadoop/file/JoinRecordWithStationName.jar root@192.168.1.124:/usr/local/test/mr hadoop jar JoinRecordWithStationName.jar com.zhen.mapreduce.join.JoinRecordWithStationName
结果如下
00001 北京 00001,20180102,16 00001 北京 00001,20180101,15 00002 天津 00002,20180102,26 00002 天津 00002,20180101,25 00003 山东 00003,20180102,36 00003 山东 00003,20180101,35