一、MR中的Counter计数器
计数器在MR中都是全局的,主要用于监控一些数据有无出入;
主要的:1、Map-Reduce FrameWork(主要是MR的输入输出等信息) 2、jobs
而如果说业务有需求,用户可以自定义计数器来统计程序执行过程中的某种情况的出现次数
自定义计数器的使用:
1、通过context.getCounter(group name,counter name)方法获取到一个全局计数器,创建的时候需要指定计数器所属的组名和计数器的名字
2、在需要用到计数器的地方进行计数器的操作
最常用的:给计数器增加指定的数值--------counter.increment(int num)
二、MR的DB操作--------对数据库的操作
1、读取mysql操作------------DBInputFormat类(这里以最简单的输出数据库信息到指定文件中为例)
Ⅰ创建对应数据库对象类(bean):
实现两个接口:Writable、DBWritable和对应方法的重写(以下是DBWritable的对应方法的重写)
Ⅱ 根据业务需求分析得知,不需要reducer类,只需要mapper类将数据库中的表的数据读取出来并以<k,v>键值对的形式输出到定向文件中,因此编写mapper类(goodsbean里面已经实现了toString的方法):
/**
* DBInputFormat类用于从数据库中读取数据。底层一行一行读取表中的数据,返回<k,v>键值对
* k是LongWritable类型,表中数据的偏移量
* todo v是DBWritable类型,表示该行数据的对象类型
*/
public class ReadDB_Mapper extends Mapper<LongWritable,GoodsBean, Text, NullWritable> {
Text outkey = new Text();
NullWritable outvalue = NullWritable.get();
@Override
protected void map(LongWritable key, GoodsBean value, Context context) throws IOException, InterruptedException {
outkey.set(value.toString());
context.write(outkey,outvalue);
}
}
//将表中的数据输出到文件中,没有reduce阶段--------具体看Dirver类
Ⅲ 在Driver类中,需要配置当前的jdbc信息(jdbc即java对数据库的一系列操作的API,详情请见:JDBC详细介绍_Jungle_Rao的博客-CSDN博客_jdbc),还有对reducetask的设置(这里为0)
public class ReadDB_Driver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
//配置文件对象
Configuration conf = new Configuration();
//todo 配置当前作业需要用到的jdbc信息
DBConfiguration.configureDB(
conf,
"com.mysql.jdbc.Driver", //jdbc的驱动
"jdbc:mysql://192.168.223.134:3306/ljh", //指定数据库
"root", //用户名
"123456" //密码
);
//创建作业的job类
Job job = Job.getInstance(conf, ReadDB_Driver.class.getSimpleName());
//设置MR程序的驱动类
job.setJarByClass(ReadDB_Driver.class);
//设置Mapper类
job.setMapperClass(ReadDB_Mapper.class);
//设置程序最终输出的k,v类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
//设置输出路径
FileOutputFormat.setOutputPath(job,new Path("D:\\mysqlOutPut"));
FileSystem fs = FileSystem.get(conf);
if(fs.exists(new Path("D:\\mysqlOutPut"))) {
fs.delete(new Path("D:\\mysqlOutPut"),true);
}
//todo 不需要用到reduce阶段,因此要把reducetask的个数设置为0
job.setNumReduceTasks(0);
//设置输入组件
job.setInputFormatClass(DBInputFormat.class);
//todo 调用setInput方法添加读取数据库的相关参数
org.apache.hadoop.mapreduce.lib.db.DBInputFormat.setInput(
job, //job对象
GoodsBean.class, //表中数据的具体实现对象
"select goodsId,goodsSn,goodsName,marketPrice,shopPrice,saleNum from `itheima_goods`", //输入语句
"select count(goodsId) from `itheima_goods`" //输入计数语句
//以上两条语句最好是现在数据库中验证能否直接使用,再写上来
);
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
2、写入mysql操作-----------DBOutPutFormat类(同样的以最简单的将文本文件读入到mysql中的一张表为例)
Ⅰ要求:
●它将reduce输出发送到sql表
●它接受<k,v>键值对,而其中的key必须是具有拓展DBWritable的类型(只会将key写道数据库当中)
Ⅱ 业务需要我们将之前从数据库中提取出来的表的数据再定向输出到数据库中的一个新的表当中,因此我们map阶段就需要提取出数据并将其初始化(这里注意,map阶段输出的key必须是实现了CompareTo方法的对象),而reduce阶段直接将自定义对象goodsbean作为key传到数据库当中实现表的书写
map阶段代码:
/**
* 在map阶段读取文本数据,并将其封装成对象(goodsbean)
*/
public class WriteDB_Mapper extends Mapper<LongWritable, Text,NullWritable,GoodsBean> {
GoodsBean outvalue = new GoodsBean();
NullWritable outkey = NullWritable.get();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//获取两个计数器,用于统计合法数据和非法数据
Counter sc= context.getCounter("mr_to_sql", "success_counter");
Counter fc = context.getCounter("mr_to_sql", "fail_counter");
String[] infos = value.toString().split("\t");
//判断输入的数据字段是否有缺失,如果不满足需求则是非法数据
if (infos.length >5)
{
//合法数据,提取字段,封装成对象,并在成功的计数器那里加一
outvalue.set(Long.parseLong(infos[0]),infos[1],infos[2],Double.parseDouble(infos[3]),Double.parseDouble(infos[4]),Long.parseLong(infos[5]));
sc.increment(1);
context.write(outkey,outvalue);
}
else
{
fc.increment(1);
}
}
}
reduce阶段代码:
/**
* 在使用DBOutPutFormat的时候,要求key必须是DBWritable的实现类
*/
public class WriteDB_Reducer extends Reducer< NullWritable,GoodsBean,GoodsBean,NullWritable> {
@Override
protected void reduce(NullWritable key, Iterable<GoodsBean> values, Context context) throws IOException, InterruptedException {
for (GoodsBean value : values) {
context.write(value,key);
}
}
}
Ⅲ 运行主类对于具体实现的配置:设置输入路径,设置输出的主类:DBOutPutFormat类,并将表的信息传进来:
//设置程序的输入路径
FileInputFormat.setInputPaths(job,new Path("D:\\mysqlOutPut"));
//设置程序的输出类
job.setOutputFormatClass(DBOutputFormat.class);
//todo 配置当前作业写入数据库中的表
DBOutputFormat.setOutput(
job,
"ljh_receive", //接收数据的表的名称
"goodsId","goodsSn","goodsName","marketPrice","shopPrice","saleNum"
//表中字段的对应关系(对应自定义对象的属性)
);
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
表(ljh_receive的配置,这里以讲义的为例---就是名字不一样而已):
三、MR的join关联操作(Map Side Join 和 Reduce Side Join)
1、reduce side join:在reduce阶段对同一个分区的数据进行join操作,一定要把join的字段设置为key,这样相同字段的数据才能来到同一个分组当中
缺点:数据量大的时候不太好处理,因为一般只有一个reducetask,这样就会导致reduce阶段的任务量过大从而影响数据处理的效率
实例-------将两个表(商品基本信息表和订单信息表)中的数据根据商品编号整合到一起,得出的表的顺序依次为:
商品id 订单编号 实际支付价格 商品编号 商品名称
map阶段代码:(需要通过重写map初始化方法setup获取切片所处文件,从而针对不同文件的数据进行切割)
public class ReduceJoin_Mapper extends Mapper<LongWritable,Text,Text,Text> {
String filename = null;
Text outkey = new Text();
Text outvalue = new Text();
StringBuilder sb = new StringBuilder();
//maptask的初始化方法,获取当前切片所属文件名称
@Override
protected void setup(Context context) throws IOException, InterruptedException {
//获取当前处理的切片
FileSplit split = (FileSplit) context.getInputSplit();
//获取当前切片所属的文件名称
filename = split.getPath().getName();
}
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//清空sb
sb.setLength(0);
//切割我们读取的一行数据
String[] fields = value.toString().split("\\|");
//根据文件名进行判断
if (filename.contains("itheima_goods.txt"))
{
//商品数据,数据格式:100101|155083444927602|四川果冻橙6个约180g/个
outkey.set(fields[0]); //商品id
StringBuilder append = sb.append(fields[1]).append("\t").append(fields[2]);//拼接剩余数据
outvalue.set(sb.insert(0,"goods#").toString()); //给商品数据开头加个前缀,用于后续判断
context.write(outkey,outvalue);
}
else
{
//订单数据,数据格式:1|107860|7191 ----订单号|商品id|实际支付价格
outkey.set(fields[1]);
StringBuilder append = sb.append(fields[0]).append("\t").append(fields[2]);
outvalue.set(sb.insert(0,"orders#").toString()); //给订单数据开头加个前缀,用于后续判断
context.write(outkey,outvalue);
}
}
}
reduce阶段代码:(简单来说可以将reducejoin这个阶段理解成一个拼接阶段,但前提是map阶段传进来的键值对的key是需要拼接的字段,然后根据自行添加的前缀来区别不同的文件数据,再对key相同的数据拼接进行整合)
public class ReduceJoin_Reducer extends Reducer<Text,Text,Text,Text> {
//创建集合用于保存订单数据和商品数据,便于后续join关联
//用于保存商品数据 商品编号 商品名称
List<String> goodsList = new ArrayList<String>();
//用于保存 订单编号 实际支付价格
List<String> orderList = new ArrayList<String>();
Text outvalue = new Text();
@Override
protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
//遍历values
for (Text value : values) {
//判断数据是订单数据还是商品数据
if (value.toString().startsWith("goods#"))
{
String s = value.toString().split("#")[1];//除去前置标签符
//添加到商品集合中
goodsList.add(s);
}
if (value.toString().startsWith("orders#"))
{
String s = value.toString().split("#")[1];//除去前置标签符
//添加到订单集合中
orderList.add(s);
}
}
//获取两个集合的长度
int goodsSize = goodsList.size();
int orderSize = orderList.size();
for (int i = 0; i < orderSize; i++) {
//其实这里的商品编号是唯一的,因此这里的goodslist的长度肯定为1
for (int j = 0; j < goodsSize; j++) {
outvalue.set(orderList.get(i)+"\t"+goodsList.get(j)); //信息的拼接
context.write(key,outvalue); //key:商品id 订单编号 实际支付价格 商品编号 商品名称
}
}
goodsList.clear(); //清空集合
orderList.clear(); //清空集合
}
}
商品长度的测试(在for里面加个输出goodSize即可测试):
dirver代码与之前最基础的一样,只是reduce阶段和map阶段的输出数据类型不同而已;
而由于我们使用的是商品编号作为key(硬性需求),因此会导致我们输出的时候的结果较为无序------因为排序的操作是在map的shuffle阶段进行的,但是以商品编号作为key肯定会导致无序;因此对于数据的可视性而言,肯定是需要将订单编号作为key传递才能让这些数据更加的整洁明了;
如果说换做我刚学MR那时候,我就会重新写一个新的MR,包括mapper,reducer,Driver,然后将这个join的输出结果作为输入文件给这个程序进行处理。这样虽然思路很清晰,但是你得创建很多个类,很耗费时间;
⭐但前不久我刚学到一个方法,就是直接将三个(mapper,reducer,Driver)放在一个程序中(join_sort),直接在一个类里面定义mapper类和reducer类,并重写map、reduce方法,不用再定义mapper和reducer类,而Driver类直接用main方法来替代(输入文件和输出文件的路径定义还是在参数中修改,输入的文件是前面join操作后输出的文件)
同理,这个方法也可以放到之前新冠疫情的处理中,有时间优化一下
public class ReduceJoin_Sort {
public static class ReducejoinSortMapper extends Mapper<LongWritable, Text,Text,Text> {
Text outkey = new Text();
Text outvalue = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//以下的数据都是以已经join完了的数据为准
String[] fields = value.toString().split("\t");
outkey.set(fields[1]);//订单编号作为key
//输出的结果:订单编号 商品id 商品编码 商品名称 实际成交价格
outvalue.set(fields[1]+"\t"+fields[0]+"\t"+fields[3]+"\t"+fields[4]+"\t"+fields[2]);
context.write(outkey,outvalue);
}
}
public static class ReducejoinSortReducer extends Reducer<Text,Text,Text, NullWritable> {
@Override
protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
for (Text value : values) {
//输出数据
context.write(value, NullWritable.get());
}
}
}
public static void main(String[] args) throws Exception {
//该类就是客户端的驱动类,主要是构造job对象实例
//指定各个组件的属性,包括mapper类、reducer类,输入输出的数据类型、输入输出的数据路径,提交jo b作业----job.submit()
//创建配置对象
Configuration conf = new Configuration();
//默认local模式运行
//构建job对象,参数(配置对象,job的名字)
Job job = Job.getInstance(conf,ReduceJoin_Sort.class.getSimpleName());
//下面是驱动的固定模板
//设置mr程序运行的主类
job.setJarByClass(ReduceJoin_Sort.class);
//设置本次mr程序的mapper类、reducer类
job.setMapperClass(ReducejoinSortMapper.class);
job.setReducerClass(ReducejoinSortReducer.class);
//指定map阶段输出的kv的数据类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(Text.class);
//指定reduce阶段输出的kv的数据类型,也就是最终输出的数据类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
//设置reducetask的个数
//job.setNumReduceTasks(3);
// TODO 设置mapreduce的Combiner类(局部单词统计的逻辑和reducer一样)
//job.setCombinerClass(wc_Ruducer.class);
//配置本次作业的输入输出路径,和输出数据路径
// TODO 默认组件:TextInputFormat---输入;TextOutPutFormat---输出
//这里指定路径要从控制台输入,更加方便
Path inuput = new Path(args[0]);
Path output = new Path(args[1]);
FileInputFormat.setInputPaths(job,inuput);
FileOutputFormat.setOutputPath(job,output);
//判断输出路径是否已经存在,如果存在就把他删除
FileSystem fs = FileSystem.get(conf);
if(fs.exists(output)) {
fs.delete(output,true);
}
//提交作业,参数表示是否实时追踪作业的执行情况
boolean resultflag = job.waitForCompletion(true);
//退出程序,与job结果进行绑定,为true则正常退出,为false则异常退出
System.exit(resultflag ? 0 :1);
}
}
四、MR的分布式缓存
如果说需要maptask从数据库中读取同一个文件的不同块的话,会导致数据库的可用性大幅度下降,甚至可能会导致数据库出问题;那么直接将数据库中的数据发送到各个maptask的话就可以极大地解决这个问题;
所以,这就是MR地分布式缓存的由来------即直接将数据库中的对应数据发送到对应的maptask,从而减少数据库的任务量;
分布式缓存的使用方式:(必须在YARN模式下才能运行)
使用MR对应的API:
步骤一----添加缓存文件(上传到HDFS中,便于缓存文件的发送):
添加归档文件到分布式缓存中--------job.addCacheArchive(URI uri);
添加普通文件到分布式缓存中--------job.addCacheFile(URI uri);
步骤二:MR程序读取缓存文件:
在mapper或者reduer类的setup方法中,用BufferReader获取分布式缓存中的文件内容(BufferedReader是带缓冲区的字符流,能够减少访问磁盘的次数,提高文件读取性能;可以一次性读取一行字符)