1. Map侧连接
Map端join是指数据到达map处理函数之前进行合并的,效率要远远高于Reduce端join,因为Reduce端join是把所有的数据都经过Shuffle,非常消耗资源。
注意:在Map端join操作中,我们往往将较小的表添加到内存中,因为内存的资源是很宝贵的,这也说明了另外一个问题,那就是如果表的数据量都非常大则不适合使用Map端join。
1.1 基本思路
- 需要join的两个文件,一个存储在HDFS中,一个在作业提交前,使用Job.addCacheFile(URI uri)将需要join的另外一个文件加入到所有Map缓存中;
- 在Mapper.setup(Context context)函数里读取该文件;
- 在Mapper.map(KEYIN key, VALUEIN value, Context context)进行join;
- 将结果输出(即没有Reduce任务)。
1.2 示例
public class ProvinceMapJoinStatistics {
public static class ProvinceLeftJoinMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
private String provinceWithProduct = "";
/**
* 加载缓存文件
*/
@Override
protected void setup(Context context) throws IOException, InterruptedException {
URI[] uri = context.getCacheFiles();
if (uri == null || uri.length == 0) {
return;
}
for (URI p : uri) {
if (p.toString().endsWith("part-r-00000")) {
// 读缓存文件
try {
provinceWithProduct = HdfsUtil.read(new Configuration(), p.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
if (!provinceWithProduct.contains(value.toString()
.substring(0, 2))) {
context.write(value, NullWritable.get());
}
}
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
if (otherArgs.length < 3) {
System.err.println("Usage: <in> [<in>...] <out>");
System.exit(2);
}
HdfsUtil.rmr(conf, otherArgs[otherArgs.length - 1]);
Job job = Job.getInstance(conf, "ProvinceMapJoinStatistics");
job.setJarByClass(ProvinceMapJoinStatistics.class);
// 设置缓存文件
job.addCacheFile(new Path(args[1]).toUri());
job.setMapperClass(ProvinceLeftJoinMapper.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
FileOutputFormat.setOutputPath(job, new Path(otherArgs[2]));
if (job.waitForCompletion(true)) {
HdfsUtil.cat(conf, otherArgs[2] + "/part-r-00000");
System.out.println("success");
} else {
System.out.println("fail");
}
}
}
2. Reduce侧连接
Reduce端连接比Map端连接更为普遍,因为输入的数据不需要特定的结构,但是效率比较低,因为所有数据都必须经过Shuffle过程。
2.1 基本思路
- Map端读取所有的文件,并在输出的内容里加上标示,代表数据是从哪个文件里来的。
- 在reduce处理函数中,按照标识对数据进行处理。
- 然后根据Key去join来求出结果直接输出。
2.2 示例
public class ReduceJoinDemo {
public static class ReduceJoinMapper extends Mapper<LongWritable, Text, Text, Text> {
public void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
// 获取输入记录的字符串
String line = value.toString();
// 抛弃空记录
if (line == null || line.equals("")) {
return;
}
// 获取输入文件的全路径和名称
FileSplit fileSplit = (FileSplit) context.getInputSplit();
String path = fileSplit.getPath().toString();
//处理来自tb_a表的记录
if (path.contains("province.txt")) {
context.write(new Text(line.substring(0, 2)), new Text("a#" + line));
} else if (path.contains("part-r-00000")) {
context.write(new Text(line.substring(0, 2)), new Text("b#"));
}
}
}
public static class ReduceJoinReducer extends Reducer<Text, Text, Text, NullWritable> {
// province.txt存在, part-r-00000不存在的数据
@Override
protected void reduce(Text key, Iterable<Text> values, Context context)
throws IOException, InterruptedException {
int count = 0;
String province = "";
for (Text value : values) {
count++;
String str = value.toString();
if (str.startsWith("a#")) {
province = str.substring(2);
}
}
if (count == 1) {
context.write(new Text(province), NullWritable.get());
}
}
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
if (otherArgs.length < 3) {
System.err.println("Usage: <in> [<in>...] <out>");
System.exit(2);
}
HdfsUtil.rmr(conf, otherArgs[otherArgs.length - 1]);
Job job = Job.getInstance(conf, ReduceJoinDemo.class.getSimpleName());
job.setJarByClass(ReduceJoinDemo.class);
job.setMapperClass(ReduceJoinMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(Text.class);
job.setReducerClass(ReduceJoinReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
FileInputFormat.setInputPaths(job, new Path(otherArgs[0]), new Path(otherArgs[1]));
FileOutputFormat.setOutputPath(job, new Path(otherArgs[2]));
if (job.waitForCompletion(true)) {
HdfsUtil.cat(conf, otherArgs[2] + "/part-r-00000");
System.out.println("success");
} else {
System.out.println("fail");
}
}
}
3. SemiJoin
SemiJoin就是所谓的半连接,其实仔细一看就是reduce join的一个变种,就是在map端过滤掉一些数据,在网络中只传输参与连接的数据不参与连接的数据不必在网络中进行传输,从而减少了shuffle的网络传输量,使整体效率得到提高,其他思想和reduce join是一模一样的。说得更加接地气一点就是将小表中参与join的key单独抽出来通过DistributedCach分发到相关节点,然后将其取出放到内存中(可以放到HashSet中),在map阶段扫描连接表,将join key不在内存HashSet中的记录过滤掉,让那些参与join的记录通过shuffle传输到reduce端进行join操作,其他的和reduce join都是一样的。
4. 参考
《精通Hadoop》 [印] Sandeep Karanth著 刘淼等译