MapReduce是一种并行计算的编程思想,在大数据领域得到了广泛的应用。MapReduce将计算过程分为“Map”和“Reduce”两个阶段,将程序部署在分布式系统上,可以大大提升计算效率。 —— [ 百度百科 ]
MongoDB作为一种优秀的NoSql数据库,其具备分布式数据库高性能、高吞吐量、多数据类型等优良特性,并且针对一些大数据量的聚合和统计分析场景,它也支持MapReduce。
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进行统计和计算。
在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
}
)
在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性能调优一样,需要开发者在具体业务场景上去逐步优化。全文纯属个人总结和归纳,如有错误之处,请多多批评和指正。