0%

Join操作

MapReduce Join 操作:大表关联小表用 Map 端,大表关联大表用 Reduce 端

数据处理中,多表关联是家常便饭——订单关联用户、日志关联商品、销售关联库存。在 SQL 里一个 JOIN 就搞定,在 MapReduce 里得自己实现。

MapReduce 提供了两种 Join 方案:

  • Reduce 端 Join:通用,任何规模都能用,但要走 Shuffle,性能一般
  • Map 端 Join:快,但要求一张表足够小,能塞进内存

怎么选?就一句话:小表能装进内存就用 Map 端 Join,装不下就用 Reduce 端 Join。

Join 的核心难点:怎么让相同 Key 的数据到同一个地方

两个表关联,关键是把相同关联键的数据凑到一起。

  • 订单表有商品 ID → 需要找到商品表里相同商品 ID 的信息
  • 问题是:订单数据分散在不同节点,商品数据也分散在不同节点
  • 怎么让同一个商品 ID 的订单数据和商品数据落到同一个节点?

这就是 Join 的核心挑战。MapReduce 用两种方式解决:

方案 怎么凑到一起 前提
Reduce 端 Join 通过 Shuffle,相同 Key 自动分到同一个 Reduce 无限制
Map 端 Join 小表提前加载到每个 Map 任务的内存里 小表能装进内存

Reduce 端 Join:通用方案,任何规模都能用

实现原理
flowchart TD  
    A[表 A 数据] -->|Map 阶段| B["打标签 "]  
    C[表 B 数据] -->|Map 阶段| D["打标签 "]  
    B --> E[Shuffle 按 Key 分组]  
    D --> E  
    E --> F["Reduce 阶段合并 "]  
    F --> G[输出关联结果]

思路:

  1. Map 阶段:从不同表读数据,统一输出 <关联键, 来源标记 + 数据>
  2. Shuffle 阶段:相同关联键自动分到同一个 Reduce
  3. Reduce 阶段:把同一 Key 的两张表数据合并

关键点:Map 阶段一定要标记数据来源。 Reduce 阶段收到的是同一个 Key 的 value 列表,不知道哪些来自订单表、哪些来自商品表,所以需要标记(比如用 “order” 和 “product” 前缀)。

代码骨架:

Map 阶段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void map(LongWritable key, Text value, Context context) {
FileSplit split = (FileSplit) context.getInputSplit();
String fileName = split.getPath().getName();

String[] fields = value.toString().split(",");

if (fileName.contains("order")) {
// 订单表:关联键是商品ID(第3列)
outKey.set(fields[2]); // 商品ID
outValue.set("order," + fields[0] + "," + fields[1] + "," + fields[3]);
context.write(outKey, outValue);
} else if (fileName.contains("product")) {
// 商品表:关联键是商品ID(第1列)
outKey.set(fields[0]); // 商品ID
outValue.set("product," + fields[1]); // 商品名称
context.write(outKey, outValue);
}
}

Reduce 阶段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void reduce(Text key, Iterable<Text> values, Context context) {
List<String> orders = new ArrayList<>();
String productName = "";

for (Text value : values) {
String[] parts = value.toString().split(",");
if (parts[0].equals("order")) {
orders.add(parts[1] + "," + parts[2] + "," + parts[3]); // 订单ID,用户ID,金额
} else if (parts[0].equals("product")) {
productName = parts[1];
}
}

// 内连接:只输出有商品信息的订单
for (String order : orders) {
if (!productName.isEmpty()) {
context.write(null, new Text(order + "," + productName));
}
}
}

优缺点:

优点 缺点
数据量无限制,什么都能 Join 所有数据都要走 Shuffle,网络传输大
支持内连接、左连接、外连接 Reduce 端需要排序,开销大
实现直观,好理解 大表 Join 大表时,Reduce 是瓶颈

Map 端 Join:快,但小表得能装进内存

实现原理

flowchart TD  
    A[小表数据] -->|Map 初始化| B["加载至内存 Map"]  
    C[大表数据] -->|Map 阶段| D["读取大表记录,通过内存 Map 关联"]  
    D --> E[直接输出关联结果]

思路: 把小表加载到每个 Map 任务的内存里,Map 读大表时直接在内存里查,不需要 Shuffle 和 Reduce。

前提条件: 小表必须能装进 MapTask 内存(通常 < 1GB)。如果装不下,Map 端 Join 不适用。

代码骨架:

setup 阶段加载小表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private HashMap<String, String> productMap = new HashMap<>();

@Override
protected void setup(Context context) {
// 从分布式缓存读取小表
URI[] cacheFiles = context.getCacheFiles();
Path productPath = new Path(cacheFiles[0]);
FileSystem fs = FileSystem.get(context.getConfiguration());
BufferedReader reader = new BufferedReader(new InputStreamReader(fs.open(productPath)));

String line;
while ((line = reader.readLine()) != null) {
String[] fields = line.split(",");
productMap.put(fields[0], fields[1]); // 商品ID → 商品名称
}
reader.close();
}

Map 阶段查内存:

1
2
3
4
5
6
7
8
9
10
public void map(LongWritable key, Text value, Context context) {
String[] fields = value.toString().split(",");
String productId = fields[2]; // 订单表的商品ID

String productName = productMap.get(productId);
if (productName != null) {
String result = fields[0] + "," + fields[1] + "," + fields[3] + "," + productName;
context.write(null, new Text(result));
}
}

Driver 里把小表放进分布式缓存:

1
2
job.addCacheFile(new URI("hdfs:///input/product.txt"));
job.setNumReduceTasks(0); // 不需要 Reduce

优缺点:

优点 缺点
无 Shuffle,无 Reduce,性能极高 小表必须能装进内存,否则 OOM
网络传输少 只适合”一大一小”的场景
代码比 Reduce 端 Join 简单 不支持大表 Join 大表

实际项目中的经验:

  • 小表 < 100MB → 无脑 Map 端 Join
  • 小表 100MB - 500MB → Map 端 Join,把 MapTask 内存调到 2GB 以上
  • 小表 > 500MB → 先用 Map 端 Join 试,不行换 Reduce 端
  • 两张都大 → Reduce 端 Join,同时用 Combiner 和压缩优化

欢迎关注我的其它发布渠道