一、前言
MapReduce的join 可以类似于sql的join ,不同的是sql是对表进行连接查询,而mapreduce的join是对文件进行连接查询。解决的问题就是不同数据的合并问题。
区别:
reduce join是在map阶段完成数据的标记,在reduce阶段完成数据的合并
map join是直接在map阶段完成数据的合并,没有reduce阶段。
二、Reduce join
2.1 工作原理
Map端的主要工作:为来自不同表或文件的k-v对,打标签,以区分不同来源的记录。然后用连接字段作为key,其余部分和新加的标志作为value,最后进行输出。
Reduce端的主要工作:在Reduce端以连接字段作为key的分组已经完成,我们只需要在每一个分组当中将那些来源不同文件的记录分开,最后进行合并就ok了。
2.2 案例实操
需求:两个文件,将两文件通过pid关联,合并到新的订单数据表,含有商品名。
order
id pid amount
1001 01 1
1002 02 2
1003 03 3
1004 01 4
1005 02 5
1006 03 6
pd
pid pname
01 小米
02 华为
03 格力
分析:通过关联条件作为map的输出key,将两表满足Join条件的数据并携带数据所来源的文件信息,发送同一个ReduceTask中,在Reduce中进行数据的串联。
orderbean
@Data
public class OrderBean implements Writable{
//订单id
private String orderId;
//商品id
private String pid;
//订单数
private Integer amount;
//商品名称
private String pname;
//分类
private String flag;
public OrderBean(){
}
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeUTF(orderId);
dataOutput.writeUTF(pid);
dataOutput.writeInt(amount);
dataOutput.writeUTF(pname);
dataOutput.writeUTF(flag);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
this.orderId=dataInput.readUTF();
this.pid=dataInput.readUTF();
this.amount=dataInput.readInt();
this.pname=dataInput.readUTF();
this.flag=dataInput.readUTF();
}
@Override
public String toString() {
return this.orderId+"\t"+this.pname+"\t"+this.amount;
}
}
Mapper
/**
* 所有类型文件,都以公共的字段pid 来关联
*/
public class OrderMapper extends Mapper<LongWritable, Text,Text,OrderBean> {
private FileSplit currentSplit;
private Text outK=new Text();
private OrderBean outV = new OrderBean();
//该方法只能执行一次
@Override
protected void setup(Context context) throws IOException, InterruptedException {
//获取文件名
currentSplit=(FileSplit) context.getInputSplit();
}
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String[] split = value.toString().split("\t");
//对不同数据来源分开处理,明确数据来自哪个文件
if(currentSplit.getPath().getName().contains("order")){
outK.set(split[1]);
outV.setOrderId(split[0]);
outV.setPid(split[1]);
outV.setAmount(Integer.parseInt(split[2]));
outV.setPname("");
outV.setFlag("order");
}else{
outK.set(split[0]);
outV.setOrderId("");
outV.setPid(split[0]);
outV.setPname(split[1]);
outV.setAmount(0);
outV.setFlag("pd");
}
context.write(outK,outV);
}
}
Reduce
public class OrderReducer extends Reducer<Text,OrderBean,OrderBean, NullWritable> {
protected List<OrderBean> list =new ArrayList<OrderBean>();
protected OrderBean pdOrder =new OrderBean();
@Override
protected void reduce(Text key, Iterable<OrderBean> values, Context context) throws IOException, InterruptedException {
//将来自order表的相同pid 数据维护到一个集合中
//来自pd表的维护成一个对象
for (OrderBean value : values) {
if("order".equals(value.getFlag())){
OrderBean orderBean = new OrderBean();
orderBean.setOrderId(value.getOrderId());
orderBean.setAmount(value.getAmount());
orderBean.setFlag(value.getFlag());
orderBean.setPname(value.getPname());
orderBean.setPid(value.getPid());
list.add(orderBean);
}else{
//来自pd
pdOrder.setPid(value.getPid());
pdOrder.setOrderId(value.getOrderId());
pdOrder.setFlag(value.getFlag());
pdOrder.setAmount(value.getAmount());
pdOrder.setPname(value.getPname());
}
}
//遍历这个list
for (OrderBean orderBean : list) {
orderBean.setPname(pdOrder.getPname());
context.write(orderBean,NullWritable.get());
}
//一组数据处理完成后将集合清空
list.clear();
}
}
driver
public class OrderDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
Job job =Job.getInstance(new Configuration());
job.setJarByClass(OrderDriver.class);
job.setMapperClass(OrderMapper.class);
job.setReducerClass(OrderReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(OrderBean.class);
job.setOutputKeyClass(OrderBean.class);
job.setOutputValueClass(NullWritable.class);
FileInputFormat.setInputPaths(job,new Path("D:/cs/join/input"));
FileOutputFormat.setOutputPath(job,new Path("D:/cs/join/output5"));
job.waitForCompletion(true);
}
}
redcue join 不好的一点是所有的东西都交给reduce来做,理论上,reduce个数是很少的,如果数据很大,而map啥都不干,把所有的数据扔给redcue。会造成redcue的压力很大,而redcue的个数不同,容易造成不同reduce的处理不同。也就是数据倾斜。
三、Map join
3.1 使用场景
map join适用于 一张表十分小、一张表十分大的场景。
3.2 优点
思考:在reduce端处理过多的表,造成数据倾斜怎么办?
在map端缓存多张表,提前处理业务逻辑,这样增加map端业务,减少reduce端数据的压力,尽可能的减少数据倾斜。
3.3 具体办法
采用DistributedCache
(1)在Mapper的setup,将文件读取到缓存集合中 (2)在驱动函数中加载缓存
3.4 实际操作
public class MapJoinDriver {
public static void main(String[] args) throws IOException, URISyntaxException, InterruptedException, ClassNotFoundException {
Configuration configuration = new Configuration();
Job job =Job.getInstance(configuration);
//在driver中 设置缓存数据 更灵活 可通过参数传入
job.addCacheFile(new URI("file:///D:/cs/join/cache/pd.txt"));
job.setJarByClass(MapJoinDriver.class);
job.setMapperClass(MapJoinMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
//摩恩是有reduce来处理过,去掉更快
job.setNumReduceTasks(0);
FileInputFormat.setInputPaths(job,new Path("D:/cs/join/input2"));
FileOutputFormat.setOutputPath(job,new Path("D:/cs/join/output103"));
job.waitForCompletion(true);
}
}
mapper
public class MapJoinMapper extends Mapper<LongWritable,Text,Text,NullWritable> {
private Map<String,String> pdMap=new HashMap<String, String>();
private Text outK=new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//读取大表的数据 1001 01 1
String line=value.toString();
//切割
String[] split=line.split("\t");
//到pdmap中获取pname
split[1]=pdMap.get(split[1]);
//将数组中的数据转换字符串
String s=split[0]+"\t"+split[1]+"\t"+split[2];
outK.set(s);
context.write(outK, NullWritable.get());
}
/**
* 在处理大表之前 先把小表的数据维护到内存中
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void setup(Context context) throws IOException, InterruptedException {
//加载缓存文件
URI[] cacheFiles=context.getCacheFiles();
//从数据中获取到缓存文件
URI cacheFile = cacheFiles[0];
//读取文件
FileSystem fileSystem = FileSystem.get(context.getConfiguration());
FSDataInputStream in =fileSystem.open(new Path(cacheFile));
//按行读取
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in,"utf8"));
String line;
while((line=bufferedReader.readLine())!=null){
//切割一行数据
// 01 小米
String[] split = line.split("\t");
pdMap.put(split[0],split[1]);
}
//关闭
IOUtils.closeStream(bufferedReader);
}
}