MapReduce表连接操作之Reduce端join

本文介绍了一种在Hadoop中使用Reduce端Join的技术实现方案,通过在Map端标记数据来源并在Reduce端处理这些标记来完成Join操作。适用于输入数据没有特定结构的情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一:背景

Reduce端连接比Map端连接更为普遍,因为输入的数据不需要特定的结构,但是效率比较低,因为所有数据都必须经过Shuffle过程。


二:技术实现

基本思路

(1):Map端读取所有的文件,并在输出的内容里加上标示,代表数据是从哪个文件里来的。

(2):在reduce处理函数中,按照标识对数据进行处理。

(3):然后根据Key去join来求出结果直接输出。


数据准备

准备好下面两张表:

(1):tb_a(以下简称表A)

  1. id name
  2. 1 北京
  3. 2 天津
  4. 3 河北
  5. 4 山西
  6. 5 内蒙古
  7. 6 辽宁
  8. 7 吉林
  9. 8 黑龙江


(2):tb_b(以下简称表B)

  1. id statyear num
  2. 1 2010 1962
  3. 1 2011 2019
  4. 2 2010 1299
  5. 2 2011 1355
  6. 4 2011 3574
  7. 4 2011 3593
  8. 9 2010 2303
  9. 9 2011 2347

#需求就是以id为key做join操作(注:上面的数据都是以制表符“\t”分割)


计算模型

整个计算过程是:

(1):在Map阶段,把所有数据标记成<key,value>的形式,其中key是id,value则根据来源不同取不同的形式:来源于A的记录,value的值为"a#"+name;来源于B的记录,value的值为"b#"+score。

(2):在reduce阶段,先把每个key下的value列表拆分为分别来自表A和表B的两部分,分别放入两个向量中。然后遍历两个向量做笛卡尔积,形成一条条最终的结果。


如下图所示:


代码实现如下:

  1. public class ReduceJoinTest {
  2. // 定义输入路径
  3. private static final String INPUT_PATH = "hdfs://liaozhongmin:9000/table_join/tb_*";
  4. // 定义输出路径
  5. private static final String OUT_PATH = "hdfs://liaozhongmin:9000/out";
  6. public static void main(String[] args) {
  7. try {
  8. // 创建配置信息
  9. Configuration conf = new Configuration();
  10. // 创建文件系统
  11. FileSystem fileSystem = FileSystem.get(new URI(OUT_PATH), conf);
  12. // 如果输出目录存在,我们就删除
  13. if (fileSystem.exists(new Path(OUT_PATH))) {
  14. fileSystem.delete(new Path(OUT_PATH), true);
  15. }
  16. // 创建任务
  17. Job job = new Job(conf, ReduceJoinTest.class.getName());
  18. //1.1 设置输入目录和设置输入数据格式化的类
  19. FileInputFormat.setInputPaths(job, INPUT_PATH);
  20. job.setInputFormatClass(TextInputFormat.class);
  21. //1.2 设置自定义Mapper类和设置map函数输出数据的key和value的类型
  22. job.setMapperClass(ReduceJoinMapper.class);
  23. job.setMapOutputKeyClass(Text.class);
  24. job.setMapOutputValueClass(Text.class);
  25. //1.3 设置分区和reduce数量(reduce的数量,和分区的数量对应,因为分区为一个,所以reduce的数量也是一个)
  26. job.setPartitionerClass(HashPartitioner.class);
  27. job.setNumReduceTasks(1);
  28. //1.4 排序
  29. //1.5 归约
  30. //2.1 Shuffle把数据从Map端拷贝到Reduce端。
  31. //2.2 指定Reducer类和输出key和value的类型
  32. job.setReducerClass(ReduceJoinReducer.class);
  33. job.setOutputKeyClass(Text.class);
  34. job.setOutputValueClass(Text.class);
  35. //2.3 指定输出的路径和设置输出的格式化类
  36. FileOutputFormat.setOutputPath(job, new Path(OUT_PATH));
  37. job.setOutputFormatClass(TextOutputFormat.class);
  38. // 提交作业 退出
  39. System.exit(job.waitForCompletion(true) ? 0 : 1);
  40. } catch (Exception e) {
  41. e.printStackTrace();
  42. }
  43. }
  44. public static class ReduceJoinMapper extends Mapper<LongWritable, Text, Text, Text>{
  45. @Override
  46. protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, Text>.Context context) throws IOException, InterruptedException {
  47. //获取输入文件的全路径和名称
  48. FileSplit fileSplit = (FileSplit) context.getInputSplit();
  49. String path = fileSplit.getPath().toString();
  50. //获取输入记录的字符串
  51. String line = value.toString();
  52. //抛弃空记录
  53. if (line == null || line.equals("")){
  54. return;
  55. }
  56. //处理来自tb_a表的记录
  57. if (path.contains("tb_a")){
  58. //按制表符切割
  59. String[] values = line.split("\t");
  60. //当数组长度小于2时,视为无效记录
  61. if (values.length < 2){
  62. return;
  63. }
  64. //获取id和name
  65. String id = values[0];
  66. String name = values[1];
  67. //把结果写出去
  68. context.write(new Text(id), new Text("a#" + name));
  69. } else if (path.contains("tb_b")){
  70. //按制表符切割
  71. String[] values = line.split("\t");
  72. //当长度不为3时,视为无效记录
  73. if (values.length < 3){
  74. return;
  75. }
  76. //获取属性
  77. String id = values[0];
  78. String statyear = values[1];
  79. String num = values[2];
  80. //写出去
  81. context.write(new Text(id), new Text("b#" + statyear + " " + num));
  82. }
  83. }
  84. public static class ReduceJoinReducer extends Reducer<Text, Text, Text, Text>{
  85. @Override
  86. protected void reduce(Text key, Iterable<Text> values, Reducer<Text, Text, Text, Text>.Context context) throws IOException, InterruptedException {
  87. //用来存放来自tb_a表的数据
  88. Vector<String> vectorA = new Vector<String>();
  89. //用来存放来自tb_b表的
  90. Vector<String> vectorB = new Vector<String>();
  91. //迭代集合数据
  92. for (Text val : values){
  93. //将集合中的数据对应添加到Vector中
  94. if (val.toString().startsWith("a#")){
  95. vectorA.add(val.toString().substring(2));
  96. } else if (val.toString().startsWith("b#")){
  97. vectorB.add(val.toString().substring(2));
  98. }
  99. }
  100. //获取两个Vector集合的长度
  101. int sizeA = vectorA.size();
  102. int sizeB = vectorB.size();
  103. //遍历两个向量将结果写出去
  104. for (int i=0; i<sizeA; i++){
  105. for (int j=0; j<sizeB; j++){
  106. context.write(key, new Text(" " + vectorA.get(i) + " " + vectorB.get(j)));
  107. }
  108. }
  109. }
  110. }
  111. }
  112. }

程序运行的结果:



细节:

(1):当map读取源文件时,如何区分出是file1还是file2?

  1. FileSplit fileSplit = (FileSplit)context.getInputSplit();
  2. String path = fileSplit.getPath().toString();

根据path就可以知道文件的来源咯。


转自:https://blog.youkuaiyun.com/lzm1340458776/article/details/42971485

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值