MongoDB中的MapReduce框架初探

    • 引言
    • MapReduce原理
    • MapReduce的基本使用
    • Spring Data MongoDB对MR的使用
    • 小结

引言

MapReduce是一种并行计算的编程思想,在大数据领域得到了广泛的应用。MapReduce将计算过程分为“Map”和“Reduce”两个阶段,将程序部署在分布式系统上,可以大大提升计算效率。 —— [ 百度百科 ]

MongoDB作为一种优秀的NoSql数据库,其具备分布式数据库高性能、高吞吐量、多数据类型等优良特性,并且针对一些大数据量的聚合和统计分析场景,它也支持MapReduce。

MapReduce原理

MongoDB中的MapReduce框架初探_第1张图片
MongoDB的MapReduce流程如上图所示,
1)Map阶段:基于query条件,映射collection中的每一行满足query条件的document,通过emit函数输出(key,value)对;
2)Shuffle阶段:把Map阶段的结果根据key进行分组,让具有相同key的键值对合成一个类似于{key:[value1,value2….]}这样的数据结构
3)Reduce阶段:将Shuffle的结果用Reduce函数进行聚合,重复Reduce过程,直至每个key都只剩一个value为止
4)Finalize阶段:非必须阶段,此阶段是对MapReduce的处理结果做进一步处理。首先基于query参数“过滤”掉一部分数据,然后在Map阶段通过emit函数输出(key,value)对;在Reduce阶段,对Map后的输出结果基于key进行统计和计算。

MapReduce的基本使用

在MongoDB的客户端下,执行如下命令可以运行MapReduce操作:

db.runCommand(
{
mapReduce: ,
map: ,
reduce: ,
finalize: ,
out: ,
query: ,
sort: ,
limit: ,
scope: ,
jsMode: ,
verbose: ,
bypassDocumentValidation: ,
collation: ,
writeConcern:
}
)

参数 类型 描述
mapReduce collection 需要进行mapReduce的文档集合
map function map函数,基于JavaScript的语法,使用emit函数输出(key,value)对
reduce function reduce函数,基于JavaScript的语法,对特定的key进行聚合
finalize function 非必须参数,finilize函数,基于JavaScript的语法,Reduce之后的额外操作
out String/document 输出参数, [详细说明]
query document 非必须参数,指定map函数执行的查询条件
sort document 非必须参数,指定排序字段
limit number 非必须参数,指定map函数的最大行数
scope document 非必须参数,为map,reduce,finalize函数指定全局变量
jsMode boolean 非必须参数,是否在map/reduce执行过程中把中间结果转成BSON格式数据,默认为false
verbose boolean 非必须参数,是否在输出时带上时间信息,默认为false
bypassDocumentValidation boolean 非必须参数,mapreduce是否可以插入无效的文档到集合
collation document 非必须参数,通过这个参数可以设置特定的排序规则, [详细说明]
writeConcern function 非必须参数,指定mapreduce客户端向服务端(分片集群)写数据的级别, [详细说明]

具体使用我们用下面这个业务场景说明一下。比如,我们有一个源数据t_stats_hour,每个document包含如下信息:监测点编号(monitorId)、记录小时点(statsHour,形如yyyymmddHH)、监测值(value),我们要计算出每个监测点的日统计值、每个小时点平均值。
废话少说,show me the code
首先,构建Map函数

map=function(){
emit({monitorId:this.monitorId,statsMonth:this.statsHour.substring(0,8)}
,{value:this.value,count:1});
}

然后,Reduce函数

reduce=function(key, values){
var value = 0;
var count = 0;
for(var i in values){
value += values[i].value;
count += values[i].count;
}
return {‘value’:value,’count’:count};
}
}

经过Reduce及过程之后,我们的日统计值实际上已经出来了,但是我们还要计算平均值,所以我们需要构建finalize函数来执行最后平均值的计算,如下所示

finalize=function(key,reducedValue){
var avgValue = 0;
if(reducedValue.count>0){
avgValue = reducedValue.value/reducedValue.count
}
reducedValue.avgValue = avgValue;
return reducedValue;
}

最后,我们把MapReduce的结果输出到stats_test这个collection中,执行如下代码

db.t_stats_hour.mapReduce( map,
reduce,
{
out: { replace: “stats_test” },
finalize: finalize
}
)

运行之后,得到结果如下
MongoDB中的MapReduce框架初探_第2张图片

Spring Data MongoDB对MR的使用

在Spring Boot的项目中,我们有时候也需要操作MongoDB的MapReduce,这一块在Spring Data MongoDB中也做了很好的支撑,我们只需要在Maven项目中加入如下引用就可以操作MongoDB了。

<dependency>
        <groupId>org.springframework.datagroupId>
        <artifactId>spring-data-mongodbartifactId>
        <version>1.10.14.RELEASEversion>
 dependency>

Spring Data MongoDB中操作MongoDB的接口是MongoOperations,其封装了几乎所有客户端操作MongoDB的方法,包括CRUD、聚合操作、MR操作等。Spring Data MongoDB操作MR的方法如下:

    public  MapReduceResults mapReduce(Query query, String inputCollectionName, String mapFunction, String reduceFunction, MapReduceOptions mapReduceOptions, Class entityClass) {
        String mapFunc = this.replaceWithResourceIfNecessary(mapFunction);
        String reduceFunc = this.replaceWithResourceIfNecessary(reduceFunction);
        DBCollection inputCollection = this.getCollection(inputCollectionName);
        MapReduceCommand command = new MapReduceCommand(inputCollection, mapFunc, reduceFunc, mapReduceOptions.getOutputCollection(), mapReduceOptions.getOutputType(), query != null && query.getQueryObject() != null?this.queryMapper.getMappedObject(query.getQueryObject(), (MongoPersistentEntity)null):null);
        this.copyMapReduceOptionsToCommand(query, mapReduceOptions, command);
        if(LOGGER.isDebugEnabled()) {
            LOGGER.debug("Executing MapReduce on collection [{}], mapFunction [{}], reduceFunction [{}]", new Object[]{command.getInput(), mapFunc, reduceFunc});
        }

        MapReduceOutput mapReduceOutput = inputCollection.mapReduce(command);
        if(LOGGER.isDebugEnabled()) {
            LOGGER.debug("MapReduce command result = [{}]", SerializationUtils.serializeToJsonSafely(mapReduceOutput.results()));
        }

        List mappedResults = new ArrayList();
        MongoTemplate.DbObjectCallback callback = new MongoTemplate.ReadDbObjectCallback(this.mongoConverter, entityClass, inputCollectionName);
        Iterator var14 = mapReduceOutput.results().iterator();

        while(var14.hasNext()) {
            DBObject dbObject = (DBObject)var14.next();
            mappedResults.add(callback.doWith(dbObject));
        }

        return new MapReduceResults(mappedResults, mapReduceOutput);
    }

输入参数说明如下

输入参数 参数说明
query Query类,map过程中的筛选条件
inputCollectionName 需要执行MR的collection名称
mapFunction JavaScript形式的map函数
reduceFunction JavaScript形式的reduce函数
mapReduceOptions MR的可选参数
entityClass MR生成的结果对应的实体

这里重点说明一下MapReduceOptions ,这个类承载着mapreduce的非必须参数的设置,除了设置输出collection,finalize函数等,它还可以设置MR输出数据的类型,MR有四种数据输出类型:inline、replace、merge、reduce。

类型 说明
inline MR的结果只输出在内存中
replace 假如collection已经存在,直接覆盖;否则新建
merge 根据collection中的(key,value)进行合并,当(key,value)都相同时,则不做任何处理,否则更新
reduce 假如collection已存在,对两个collection基于key再次reduce计算,然后覆盖结果;否则直接新建collection

同样是基于上面的业务场景,代码示例如下

public class MRResult implements Serializable{

    private static final long serialVersionUID = 1L;

    @Setter @Getter private String id;
    @Setter @Getter private String value;

    @Override
    public String toString() {
        return "MRResult [id=" + id + ", value=" + value + "]";
    }
}
@Slf4j
public class StatsTestService {

    @Autowired
    private MongoOperations mongoOperations;

    private static final String COLLECTION_NAME = "t_stats_hour";

    private static final String OUT_PUT_COLLECTION_NAME = "stats_test";

    /**
     * 通过mapreduce对小时表进行日统计
     */
    public void calByMapreduce(){
        String mapFunction = "function(){ \n" +  
        "emit({monitorId:this.monitorId,statsMonth:this.statsHour.substring(0,8)},{value:this.value,count:1}); }"; //map函数
        String reduceFunction = "function(key, values){\n" +
                "\tvar value = 0;\n" +
                "\tvar count = 0;\n" +
                "\tfor(var i in values){\n" +
                "\t\tvalue += values[i].value;\n" +
                "\t\tcount += values[i].count;\t\t\n" +
                "\t}\n" +
                "\treturn {'value':value,'count':count};\n" +
                "}";//reduce函数
        String finalizeFunction = "function(key,reducedValue){\n" +
                "\tvar avgValue = 0;\n" +
                "\tif(reducedValue.count>0){\n" +
                "\t\tavgValue = reducedValue.value/reducedValue.count\n" +
                "\t}\n" +
                "\treducedValue.avgValue = avgValue;\n" +
                "\treturn reducedValue;\n" +
                "}";//最后执行函数
        MapReduceOptions mapReduceOptions = MapReduceOptions.options();
        mapReduceOptions.outputTypeMerge();//out:merge
        mapReduceOptions.finalizeFunction(finalizeFunction);
        mapReduceOptions.outputCollection(OUT_PUT_COLLECTION_NAME);
        MapReduceResults valueObjects =
                mongoOperations.mapReduce(COLLECTION_NAME, mapFunction, reduceFunction, mapReduceOptions, MRResult.class);
        log.info("=======>>>>>>OutputCollection:"+valueObjects.getOutputCollection()+
                ",rawResult:"+valueObjects.getRawResults()+",counts:"+valueObjects.getCounts());
        for(MRResult object:valueObjects){
            log.info("========id:"+object.getId()+",value:"+object.getValue());
        }
    }
}
ps:Spring Data MongoDB 2.X对于MapReduce的操作有一些坑,最开始我本来是想基于Spring Data MongoDB 2.0.9.RELEASE这个版本开发的,然后在测试过程中发现MR的结果怎么也不写到collection里边,这个过程中做了很多试验均无效,后面看了源码才发现核心代码中没有对outputcollection进行处理,所以提醒用2.X的小伙伴们慎用。

小结

本文基于自己的一些实际开发经历,简单的介绍了MongoDB的MapReduce基本使用,MapReduce是个很强大的并行计算工具,在具体的大数据场景有很多性能优化之处,就如SQL性能调优一样,需要开发者在具体业务场景上去逐步优化。全文纯属个人总结和归纳,如有错误之处,请多多批评和指正。

你可能感兴趣的:(大数据,MongoDB,MapReduce,MongoDB,Spring,Data,大数据,分布式)