MapReduce表连接之半连接SemiJoin

本文介绍Hadoop中半连接(SemiJoin)的概念及其在MapReduce任务中的应用,通过Map阶段过滤减少Reduce端处理数据量,有效提升Join操作性能。

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

一:背景

SemiJoin,一般称为半连接,其原理是在Map端过滤掉一些不需要join的数据,从而大大减少了reduce和Shuffle的时间,因为我们知道,如果仅仅使用Reduce端连接,那么如果一份数据,存在大量的无效数据,而这些数据在join中并不需要,但是因为没有做过预处理,所以这些数据直到真正执行reduce函数时,才被定义为无效数据,但是这个时候已经执行过了Shuffle、merge还有sort操作,所以这部分无效的数据就浪费了大量的网络IO和磁盘IO,所以在整体来讲,这是一种降低性能的表现,如果存在的无效数据越多,那么这种趋势就越明显。之所以会出现半连接,这其实是reduce端连接的一个变种,只不过是我们在Map端过滤掉了一些无效的数据,所以减少了reduce过程的Shuffle时间,所以能获取一个性能的提升。


二:技术实现

(1):利用DistributedCache将小表分发到各个节点上,在Map过程的setup()函数里,读取缓存里的文件,只将小表的连接键存储在hashSet中。

(2):在map()函数执行时,对每一条数据进行判断,如果这条数据的连接键为空或者在hashSet里不存在,那么则认为这条数据无效,使条数据也不参与reduce的过程。

注:从以上步骤就可以发现,这种做法很明显可以提升join性能,但是要注意的是小表的key如果非常大的话,可能会出现OOM的情况,这时我们就需要考虑其他的连接方式了。


测试数据如下:

/semi_jon/a.txt:

  1. 1,三劫散仙,13575468248
  2. 2,凤舞九天,18965235874
  3. 3,忙忙碌碌,15986854789
  4. 4,少林寺方丈,15698745862

/semi_join/b.txt:

  1. 3,A,99,2013-03-05
  2. 1,B,89,2013-02-05
  3. 2,C,69,2013-03-09
  4. 3,D,56,2013-06-07
  5. 5,E,100,2013-09-09
  6. 6,H,200,2014-01-10

#需求就是对上面两个表做半连接。


实现代码如下:

  1. public class SemiJoin {
  2. // 定义输入路径
  3. private static String INPUT_PATH1 = "";
  4. private static String INPUT_PATH2 = "";
  5. // 定义输出路径
  6. private static String OUT_PATH = "";
  7. public static void main(String[] args) {
  8. try {
  9. // 创建配置信息
  10. Configuration conf = new Configuration();
  11. // 获取命令行的参数
  12. String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
  13. // 当参数违法时,中断程序
  14. if (otherArgs.length != 3) {
  15. System.err.println("Usage:Semi_join<in1> <in2> <out>");
  16. System.exit(1);
  17. }
  18. // 给路径赋值
  19. INPUT_PATH1 = otherArgs[0];
  20. INPUT_PATH2 = otherArgs[1];
  21. OUT_PATH = otherArgs[2];
  22. // 把小表添加到共享Cache里
  23. DistributedCache.addCacheFile(new URI(INPUT_PATH1), conf);
  24. // 创建文件系统
  25. FileSystem fileSystem = FileSystem.get(new URI(OUT_PATH), conf);
  26. // 如果输出目录存在,我们就删除
  27. if (fileSystem.exists(new Path(OUT_PATH))) {
  28. fileSystem.delete(new Path(OUT_PATH), true);
  29. }
  30. // 创建任务
  31. Job job = new Job(conf, SemiJoin.class.getName());
  32. // 设置成jar包
  33. job.setJarByClass(SemiJoin.class);
  34. //1.1 设置输入目录和设置输入数据格式化的类
  35. FileInputFormat.setInputPaths(job, INPUT_PATH2);
  36. job.setInputFormatClass(TextInputFormat.class);
  37. //1.2设置自定义Mapper类和设置map函数输出数据的key和value的类型
  38. job.setMapperClass(SemiJoinMapper.class);
  39. job.setMapOutputKeyClass(Text.class);
  40. job.setMapOutputValueClass(CombineEntity.class);
  41. //1.3 设置分区和reduce数量(reduce的数量,和分区的数量对应,因为分区为一个,所以reduce的数量也是一个)
  42. job.setPartitionerClass(HashPartitioner.class);
  43. job.setNumReduceTasks(1);
  44. //1.4 排序
  45. //1.5 归约
  46. //2.1 Shuffle把数据从Map端拷贝到Reduce端。
  47. //2.2 指定Reducer类和输出key和value的类型
  48. job.setReducerClass(SemiJoinReducer.class);
  49. job.setOutputKeyClass(Text.class);
  50. job.setOutputValueClass(Text.class);
  51. //2.3 指定输出的路径和设置输出的格式化类
  52. FileOutputFormat.setOutputPath(job, new Path(OUT_PATH));
  53. job.setOutputFormatClass(TextOutputFormat.class);
  54. // 提交作业 退出
  55. System.exit(job.waitForCompletion(true) ? 0 : 1);
  56. } catch (Exception e) {
  57. e.printStackTrace();
  58. }
  59. }
  60. /**
  61. * 自定义Mapper函数
  62. *
  63. * @author 廖*民 time : 2015年1月21日下午8:40:43
  64. * @version
  65. */
  66. public static class SemiJoinMapper extends Mapper<LongWritable, Text, Text, CombineEntity> {
  67. // 创建相关对象
  68. private CombineEntity combine = new CombineEntity();
  69. private Text flag = new Text();
  70. private Text joinKey = new Text();
  71. private Text secondPart = new Text();
  72. // 存储小表的key
  73. private HashSet<String> joinKeySet = new HashSet<String>();
  74. @Override
  75. protected void setup(Mapper<LongWritable, Text, Text, CombineEntity>.Context context) throws IOException, InterruptedException {
  76. // 读取文件流
  77. BufferedReader br = null;
  78. String temp = "";
  79. // 获取DistributedCached里面的共享文件
  80. Path[] paths = DistributedCache.getLocalCacheFiles(context.getConfiguration());
  81. System.out.println("=================================>"+paths.length);
  82. // 遍历path数组
  83. for (Path path : paths) {
  84. if (path.getName().endsWith("a.txt")) {
  85. // 创建读取文件流
  86. br = new BufferedReader(new FileReader(path.toString()));
  87. // 读取数据
  88. while ((temp = br.readLine()) != null) {
  89. // 按","切割
  90. String[] splits = temp.split(",");
  91. // 将key加入小表中
  92. joinKeySet.add(splits[0]);
  93. }
  94. }
  95. }
  96. }
  97. protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, CombineEntity>.Context context) throws IOException,
  98. InterruptedException {
  99. // 获取文件输入路径
  100. String pathName = ((FileSplit) (context.getInputSplit())).getPath().toString();
  101. System.out.println("Map中获取的路径没有a.txt吧?"+pathName);
  102. if (pathName.endsWith("a.txt")) {
  103. String[] valuesTemps = value.toString().split(",");
  104. System.out.println("进入a.txt==================>a中的字符串:"+value.toString());
  105. // 在这里过滤必须要连接的字符
  106. if (joinKeySet.contains(valuesTemps[0])) {
  107. // 设置标志位
  108. flag.set("0");
  109. // 设置连接键
  110. joinKey.set(valuesTemps[0]);
  111. // 设置第二部分
  112. secondPart.set(valuesTemps[1] + "\t" + valuesTemps[1]);
  113. // 封装实体
  114. combine.setFlag(flag);
  115. combine.setJoinKey(joinKey);
  116. combine.setSecondPart(secondPart);
  117. // 写出去
  118. context.write(combine.getJoinKey(), combine);
  119. } else {
  120. System.out.println("a.txt里");
  121. System.out.println("小表中没有此记录");
  122. for (String v : valuesTemps) {
  123. System.out.println(v + " ");
  124. }
  125. return;
  126. }
  127. } else if (pathName.endsWith("b.txt")) {
  128. System.out.println("进入b.txt==================>b中的字符串:"+value.toString());
  129. // 切割
  130. String[] valueItems = value.toString().split(",");
  131. // 判断是否在集合中
  132. if (joinKeySet.contains(valueItems[0])) {
  133. // 设置标志位
  134. flag.set("1");
  135. // 设置连接键
  136. joinKey.set(valueItems[0]);
  137. // 设置第二部分数据,注意:不同文件的列数不一样
  138. secondPart.set(valueItems[1] + "\t" + valueItems[2] + "\t" + valueItems[3]);
  139. // 封装实体
  140. combine.setFlag(flag);
  141. combine.setJoinKey(joinKey);
  142. combine.setSecondPart(secondPart);
  143. // 写出去
  144. context.write(combine.getJoinKey(), combine);
  145. } else {
  146. System.out.println("b.txt里");
  147. System.out.println("小表中没有此记录");
  148. for (String v : valueItems) {
  149. System.out.println(v + " ");
  150. }
  151. return;
  152. }
  153. }
  154. }
  155. }
  156. /**
  157. * 自定义Reducer函数
  158. *
  159. * @author 廖*民 time : 2015年1月21日下午8:41:01
  160. * @version
  161. */
  162. public static class SemiJoinReducer extends Reducer<Text, CombineEntity, Text, Text> {
  163. // 存储一个分组中左表信息
  164. private List<Text> leftTable = new ArrayList<Text>();
  165. // 存储一个分组中右表数据
  166. private List<Text> rightTable = new ArrayList<Text>();
  167. private Text secondPart = null;
  168. private Text outPut = new Text();
  169. // 一个分组调用一次reduce()函数
  170. protected void reduce(Text key, Iterable<CombineEntity> values, Reducer<Text, CombineEntity, Text, Text>.Context context) throws IOException,
  171. InterruptedException {
  172. // 清空分组数据
  173. leftTable.clear();
  174. rightTable.clear();
  175. // 将不同文件的数据,分别放在不同的集合中;注意数据过大时,会出现OOM
  176. for (CombineEntity val : values) {
  177. this.secondPart = new Text(val.getSecondPart().toString());
  178. System.out.println("传到reduce中的secondPart部分:" + this.secondPart);
  179. System.out.println("难道A表中就没有数据:" + val.getFlag().toString().trim().equals("0"));
  180. // 左表
  181. if (val.getFlag().toString().trim().equals("0")) {
  182. leftTable.add(secondPart);
  183. } else if (val.getFlag().toString().trim().equals("1")) {
  184. rightTable.add(secondPart);
  185. }
  186. }
  187. for (Text val : leftTable){
  188. System.out.println("A 表中的数据为:" + val);
  189. }
  190. for (Text val : rightTable){
  191. System.out.println("B 表中的数据为:" + val);
  192. }
  193. // 做笛卡尔积输出我们想要的连接数据
  194. for (Text left : leftTable) {
  195. for (Text right : rightTable) {
  196. outPut.set(left + "\t" + right);
  197. // 将数据写出
  198. context.write(key, outPut);
  199. }
  200. }
  201. }
  202. }
  203. }
  204. /**
  205. * 自定义实体
  206. *
  207. * @author 廖*民 time : 2015年1月21日下午8:41:18
  208. * @version
  209. */
  210. class CombineEntity implements WritableComparable<CombineEntity> {
  211. // 连接key
  212. private Text joinKey;
  213. // 文件来源标志
  214. private Text flag;
  215. // 除了键外的其他部分的数据
  216. private Text secondPart;
  217. // 无参构造函数
  218. public CombineEntity() {
  219. this.joinKey = new Text();
  220. this.flag = new Text();
  221. this.secondPart = new Text();
  222. }
  223. // 有参构造函数
  224. public CombineEntity(Text joinKey, Text flag, Text secondPart) {
  225. this.joinKey = joinKey;
  226. this.flag = flag;
  227. this.secondPart = secondPart;
  228. }
  229. public Text getJoinKey() {
  230. return joinKey;
  231. }
  232. public void setJoinKey(Text joinKey) {
  233. this.joinKey = joinKey;
  234. }
  235. public Text getFlag() {
  236. return flag;
  237. }
  238. public void setFlag(Text flag) {
  239. this.flag = flag;
  240. }
  241. public Text getSecondPart() {
  242. return secondPart;
  243. }
  244. public void setSecondPart(Text secondPart) {
  245. this.secondPart = secondPart;
  246. }
  247. public void write(DataOutput out) throws IOException {
  248. this.joinKey.write(out);
  249. this.flag.write(out);
  250. this.secondPart.write(out);
  251. }
  252. public void readFields(DataInput in) throws IOException {
  253. this.joinKey.readFields(in);
  254. this.flag.readFields(in);
  255. this.secondPart.readFields(in);
  256. }
  257. public int compareTo(CombineEntity o) {
  258. return this.joinKey.compareTo(o.joinKey);
  259. }
  260. }

打成jar包,运行命令如下:

hadoop jar join.jar /semi_join/a.txt /semi_join/* /out

注:a.txt是要加入到内存的表,/semi_join/*是要进入map()函数进行比对的目录,/out是输出目录。


程序运行的结果为:




注:这个项目貌似也要打成jar包才能运行,在Eclipse中运行不了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值