3.3 OutputFormat 数据输出
OutputFormat:将key-value的格式的数据写回到文件
OutputFormat源码:
public abstract class OutputFormat<K, V> {
public OutputFormat() {
}
//对于给定的的任务,获取一个RecordWriter,接受KV值并处理的方法
public abstract RecordWriter<K, V> getRecordWriter(TaskAttemptContext var1) throws IOException, InterruptedException;
//检查OutputFormat的输出参数
public abstract void checkOutputSpecs(JobContext var1) throws IOException, InterruptedException;
//获取一个Output的提交器,保证output被正确的提交
public abstract OutputCommitter getOutputCommitter(TaskAttemptContext var1) throws IOException, InterruptedException;
}
3.3.1 OutputFormat接口实现类
OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了OutputFormat的接口。
1)文本输出 TextOutputFormat
默认输出格式是TextOutputFormat,它把每条记录写为文本行。它的键和值可以是任意类型,因为TextOutputFormat调用toString()方法把他们转换成字符串。
2)SequenceFileOutputFormat
将SequenceFileOutputFormat输出作为后续MapReduce任务的输入,这便是一种好的输出合适,因为格式紧凑,容易被压缩。
3)自定义OutputFormat
根据用户需求,自定义实现输出。
3.3.2 自定义OutputFormat
1)使用场景:为了实现控制最终文件的输出路径和输出格式,可以自定义OutputFormat。如要在MapReduce程序中根据数据的不同输出两类结果到不同目录,这类灵活的输出需求可以通过自定义OutputFormat来实现。
2)自定义OutputFormat步骤:
自定义一个类继承FileOutputFormat,然后改写RecordWriter,具体改写输出数据的方法write。
3)案例需求
http://www.baidu.com
http://www.google.com
http://cn.bing.com
http://www.atguigu.com
http://www.sohu.com
http://www.sina.com
http://www.sin2a.com
http://www.sin2desa.com
http://www.sindsafa.com
将以上面输入分成两个文件输出,有atguigu的放在一个文件,其余的放在一个文件。
自定义OutputFormat类
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class MyOutputFormat extends FileOutputFormat<LongWritable, Text> {
/**
* 返回一个处理数据的Record Writer
* @param taskAttemptContext
* @return
* @throws IOException
* @throws InterruptedException
*/
@Override
public RecordWriter<LongWritable, Text> getRecordWriter(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
return new MyRecordWriter(taskAttemptContext);
}
}
改写RecordWriter
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import java.io.IOException;
/**
* 将数据按照包不包含atguigu分别输出到两个文件
*/
public class MyRecordWriter extends RecordWriter<LongWritable, Text> {
FSDataOutputStream atguigu = null;
FSDataOutputStream other = null;
public MyRecordWriter(TaskAttemptContext taskAttemptContext) throws IOException {
Configuration configuration = taskAttemptContext.getConfiguration();
String outDir = configuration.get("mapreduce.output.fileoutputformat.outputdir");//获取输出目录
FileSystem fileSystem = FileSystem.get(configuration);
atguigu = fileSystem.create(new Path(outDir + "/atguigu.log"));
other = fileSystem.create(new Path(outDir + "/other.log"));
}
/**
* 接受key-value对,并按照值的不同写出到不同文件
*
* @param longWritable 读取这一行的偏移量
* @param text 读取这一行内容
* @throws IOException
* @throws InterruptedException
*/
@Override
public void write(LongWritable longWritable, Text text) throws IOException, InterruptedException {
//获取一行数据
String line = text.toString() + "\r\n";//待换行的数据
if (line.contains("atguigu")) {
//向atguigu文件写数据
atguigu.write(line.getBytes());
} else {
//向other文件写数据
other.write(line.getBytes());
}
}
/**
* 关闭资源
*
* @param taskAttemptContext
* @throws IOException
* @throws InterruptedException
*/
@Override
public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
IOUtils.closeStream(atguigu);
IOUtils.closeStream(other);
}
}
Dirver类中设置自定义的OutputFormat:
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class OutputDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Job job = Job.getInstance(new Configuration());
job.setJarByClass(OutputDriver.class);
job.setOutputFormatClass(MyOutputFormat.class);//设置自己新建的OutputFormat类
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job,new Path(args[1]));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
测试输出:
atguigu.log:
http://www.atguigu.com
other.log:
http://www.baidu.com
http://www.google.com
http://cn.bing.com
http://www.sohu.com
http://www.sina.com
http://www.sin2a.com
http://www.sin2desa.com
http://www.sindsafa.com
3.4 MapReduce工作流程
3.5 Join
3.5.1 Reduce join
Map端的主要工作:为来自不同表或文件的key/value对,打标签以区别不同来源的记录。然后用连接字段作为key,其余部分和新加的标志作value,最后进行输出。
Reduce端主要工作:在Reduce端以连接字段作为key的分组已经完成,我们需要在每一个分组当中将那些来源于不同文件的记录(在Mapj阶段已经打标志)分开,最后进行合并就完成了。
Reduce join 案例实操
1)需求
order.txt:
id pid amount
1001 01 1
1002 02 2
1003 03 3
1004 01 4
1005 02 5
1006 03 6
pd.txt
pid name
01 小米
02 华为
03 格力
将商品信息表中的数据根据商品pid合并到订单数据表中。
id pname amount
1001 小米 1
1004 小米 4
1002 华为 2
1005 华为 5
1003 格力 3
1006 格力 6
2)需求分析
通过将关联条件作为Map输出的key,将两表满足的join条件的数据并携带数据所来源的文件信息,法网同一个ReduceTask,在Reduce中进行文件的串联。
3)代码实现:
1.创建商品和订单合并后的bean类。并按照pid分组,组内按照pname降序排列。
OrderBean.java:
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class OrderBean implements WritableComparable<OrderBean> {
private String id;
private String pid;
private int amount;
private String pname;
@Override
public String toString() {
return id + "\t" + pname + "\t" + amount;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPid() {
return pid;
}
public void setPid(String pid) {
this.pid = pid;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
public String getPname() {
return pname;
}
public void setPname(String pname) {
this.pname = pname;
}
/**
* 序列化
*
* @param dataOutput
* @throws IOException
*/
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeUTF(id);
dataOutput.writeUTF(pid);
dataOutput.writeInt(amount);
dataOutput.writeUTF(pname);
}
/**
* 反序列化
*
* @param dataInput
* @throws IOException
*/
@Override
public void readFields(DataInput dataInput) throws IOException {
this.id = dataInput.readUTF();
this.pid = dataInput.readUTF();
this.amount = dataInput.readInt();
this.pname = dataInput.readUTF();
}
/**
* 按照pid分组排序,组内按照pname降序排列
*
* @param o
* @return
*/
@Override
public int compareTo(OrderBean o) {
int i = this.pid.compareTo(o.pid);
if (i != 0) {
return i;
} else {
return o.getPname().compareTo(this.getPname());
}
}
}
2.实现分组比较器 OrderComparator.java
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;
/**
* 分组比较器,按照Order的pid分组
*/
public class OrderComparator extends WritableComparator {
protected OrderComparator() {
super(OrderBean.class, true);
}
/**
* 按照PID比较a和b
*
* @param a
* @param b
* @return
*/
@Override
public int compare(WritableComparable a, WritableComparable b) {
OrderBean oa = (OrderBean) a;
OrderBean ob = (OrderBean) b;
return oa.getId().compareTo(ob.getId());
}
}
3.OrderMapper.java 按照数据来源的不同分别封装,发给同一个ReduceTask。
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.IOException;
public class OrderMapper extends Mapper<LongWritable, Text, OrderBean, NullWritable> {
private OrderBean order = new OrderBean();
private String filename;
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//获取输入数据的文件名
FileSplit fs = (FileSplit) context.getInputSplit();
filename = fs.getPath().getName();
//切分
String[] fields = value.toString().split("\t");
//封装按照数据的来源不同分别分装
if ("order.txt".equals(filename)) {
//封装order5
order.setId(fields[0]);
order.setPid(fields[1]);
order.setAmount(Integer.parseInt(fields[2]));
order.setPname("");//没有用到的地方封装空值
} else {
//封装pd
order.setPid(fields[0]);
order.setPname(fields[1]);
order.setAmount(0);
order.setId("");
}
context.write(order, NullWritable.get());
}
}
4.OrderReducer.java 对每一组,取第一组数据的pname,置换组中其余数据,然后写出。
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.util.Iterator;
public class OrderReducer extends Reducer<OrderBean, NullWritable, OrderBean, NullWritable> {
/**
* 收到的数据,pd在一行的开头,order紧随其后
*
* @param key
* @param values
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void reduce(OrderBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
//获取迭代器
Iterator<NullWritable> iterator = values.iterator();
//迭代第一组数据
iterator.next();
String pname = key.getPname();
//迭代剩余的数据,写出并输出
while (iterator.hasNext()) {
iterator.next();
key.setPname(pname);//设置pname
context.write(key, NullWritable.get());
}
}
}
5.OrderDriver.java 驱动
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class OrderDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Path inputPath = new Path("E:\\笔记\\大数据项目\\input");
Path outPath = new Path("E:\\笔记\\大数据项目\\output");
Job job = Job.getInstance(new Configuration());
job.setJarByClass(OrderDriver.class);
job.setMapperClass(OrderMapper.class);
job.setReducerClass(OrderReducer.class);
job.setGroupingComparatorClass(OrderComparator.class);//设置比较器
job.setMapOutputKeyClass(OrderBean.class);
job.setMapOutputValueClass(NullWritable.class);
job.setOutputKeyClass(OrderBean.class);
job.setOutputValueClass(NullWritable.class);
FileInputFormat.setInputPaths(job, inputPath);
FileOutputFormat.setOutputPath(job, outPath);
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
输出结果:
1001 小米 1
1001 小米 1
1002 华为 2
1002 华为 2
1003 格力 3
1003 格力 3
3.5.2 Map Join
1)使用场景
Map Join使用于一张表十分小,一张表很大的场景。
2)优点
在Reduce端处理过多的表,非常容易产生数据倾斜,于是可以在Map端缓存多张表,提前处理业务逻辑,这样增加Map端业务,减少Reduce端数据的压力,尽可能减少数据倾斜。
MapJoin 案例实操
3)需求:同案例3.5.2的情形
4)案例分析:
MapJoin适用于关联表中有小表的情形。将pd.txt加载到内存的缓存中,建立一个HashMap对应关系,对于大表order.txt,对读入的每一行进行切割,替换(查询Hash表中的键值对),写回。不需要reduce的过程。
5)代码实现
1.驱动类 MJDriver.java
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
public class MJDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException, URISyntaxException {
Path inputPath = new Path("E:/笔记/大数据项目/input/order.txt");
Path outputPath = new Path("E:/笔记/大数据项目/output");
Job job = Job.getInstance(new Configuration());
job.setJarByClass(MJDriver.class);
job.setMapperClass(MJMapper.class);
job.setNumReduceTasks(0);//不需要reduce阶段
//设置分布式缓存,pd文件以缓存都进去 以供Mapper来读取
//job.addCacheArchive(URI.create("file:///E:/input/pd.txt"));
job.addCacheFile(new URI("file:///E:/笔记/大数据项目/input/pd.txt"));
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
FileInputFormat.setInputPaths(job, inputPath);
FileOutputFormat.setOutputPath(job, outputPath);
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
2.MJMapper.java
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.shaded.org.apache.commons.lang.StringUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
public class MJMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
private Map<String, String> pMap = new HashMap<>();
private Text k = new Text();
/**
* 读取pd到pMap
*
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void setup(Context context) throws IOException, InterruptedException {
//开流
URI[] cacheFiles = context.getCacheFiles();//获取一个数组 文件路径
FileSystem fileSystem = FileSystem.get(context.getConfiguration());
FSDataInputStream pd = fileSystem.open(new Path(cacheFiles[0]));
//将文件按行处理,读取到pMap中
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(pd));//字节流转换为字符流
String line;
while (StringUtils.isNotEmpty(line = bufferedReader.readLine())) {
String[] fields = line.split("\t");
pMap.put(fields[0], fields[1]);
}
//关流
IOUtils.closeStream(bufferedReader);
}
/**
* 处理order.txt的数据
*
* @param key
* @param value
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String[] fields = value.toString().split("\t");
k.set(fields[0] + "\t" + pMap.get(fields[1]) + "\t" + fields[2]);//将pid替换
context.write(k, NullWritable.get());//写回
}
}
输出:
1001 小米 1
1002 华为 2
1003 格力 3
1004 小米 4
1005 华为 5
1006 格力 6
3.6 MapReduce开发总结
3.6.1 输入数据接口:InputFormat
1)默认使用的实现类是:TextInputFormat
2)TextInputFormat的功能逻辑是:一次读一行文本,然后将改行起始的偏移量作为key,行内容作为value返回。
3)CombineTextInputFormat,可以把多个小文件合并成一个切片处理,提高处理效率。
3.6.2 逻辑处理接口:Mapper
用户根据业务需求实现其中三个方法:map() setup() cleanup()
3.6.3 Partitioner分区
1)默认实现HashPartitioner,逻辑是根据key的哈希值和numReduces来返回一个分区号,key.hashCode()&Integer.MAXVALUE%numReduces。
2)如果业务上有特别的需求,可以自定义分区。
3.6.4 Comparable排序
1)当我们用自定义的对象作为key来输出时,就必须要实现WriteableComparable接口,重写其中的compareTo()方法。
2)部分排序:对最终输出的每一个文件进行内部排序。
3)全排序:对所有数据进行排序,通常只有一个Reduce。
4)二次排序:排序的条件有两个。
3.6.5 Combiner合并
Combiner合并可以提高程序的执行效率,减少IO传输。但是使用时必须不能影响原有业务的处理结果。
3.6.6 逻辑处理接口:Reducer
用户根据业务需求实现其中三个方法:reduce(),setup(),cleanup()
3.6.7 输出数据接口:OutputFormat
1)默认实现类时TextOutputFormat,功能逻辑时:将每个KV对,向目标文本文件输出一行。
2)将SequenceFileOutputFormat输出作为后续MapReduce任务的输入,这便是一种好的输出格式,因为它的格式紧凑,很容易被压缩。
2)用户还可以自定OutputFormat。