前言
在使用mongodb的时候,经常会有这样的业务场景,比如搜索某个条件,然后这个条件的结果有几十万甚至几百万,然后一时半会处理不过来,就需要使用遍历循环来处理。一般来说遍历大量的数据有三种方法:
- 第一种就是用mongodb自带的游标去遍历
- 第二种是用排序然后取最后一个id去遍历
- 第三种是使用limit和skip去遍历
当数据量很少的时候可以使用第三种方法遍历,其他时候均不适合使用第三种方法遍历。本文主要对比第一种和第二种方法的优劣
使用游标遍历
一般来说直接使用mongodb的find查询,会返回一个游标,默认是返回20条,使用游标的next()方法可以继续访问下一页,类似一个翻页器。但是要注意,不要轻易的去调用游标的toArray()方法,除非你在确定返回结果数量的情况下,否则游标会把所有数据加载到内存。游标可以通过batchSize来设置每页的数量
游标需要注意的地方
首先,游标是一个内存的状态,在默认配置下,一个游标在两次getmore间隔超过10分钟,那么这个游标就会被回收,也就是说在批量处理数据的时候,如果发生卡顿或者执行时间超过预期,就有可能导致当前游标被回收,然后无法继续遍历,报错找不到游标。当然可以调整这个延迟时间或者缩小批量的数量来避免这个问题 其次,游标的本质是数据库的一个指针,指向了数据的地址,所以当数据发生变化的时候,可能会出现混乱的情况。
游标的返回是不保证顺序的,如果使用排序,会占用大量的资源。同时因为不保证顺序的情况,遍历是无法暂停后继续的。
示例代码
首先导入maven依赖
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
然后java示例
java
public void loopCollection() {
String collectionName = "test_table";
// 获取集合
MongoCollection<Document> collection = mongoTemplate.getCollection(collectionName);
// 执行查询,获取游标
MongoCursor<Document> cursor = collection.find().iterator();
// 遍历游标
while (cursor.hasNext()) {
Document document = cursor.next();
// 将 Document 转换为 JSONObject
JSONObject jsonObject = new JSONObject(document.toJson());
// 处理每个 JSONObject
}
// 关闭游标
cursor.close();
}
使用排序遍历
一般来说,排序遍历是使用某个唯一字段作为排序来遍历,每次都取结果的最后一个数据的这个字段来作为下一次查询的条件,使用limit来控制性能。比如:通过_id来遍历一个数据集合,先使用limit(100)拿到100条数据,然后取最后一个数据的_id假设为idA,然后在下一次遍历的时候加入条件 {"_id":{$gt:idA}然后继续limit(100),以此类推,来达到遍历的效果
排序遍历需要注意的地方
排序遍历每次都会使用排序,当条件很简单或者是遍历所有数据的时候,这种方法是性能和准确性的最佳方法,同时每次遍历数据消耗的性能都是比较平均的,不容易造成数据库性能拥堵。 排序遍历在条件比较复杂的情况下,性能可能受索引的影响,在条件很多的情况下,排序遍历挺难所有的查询都使用索引,特别是_id排序,往往后面的遍历只会使用的到_id的索引。所以条件复杂的时候需要测试性能来避免遍历引起过多数据库开销。
排序遍历的java实现
以springboot来说,以下是排序遍历的一个java工具,大家可以直接复制使用
java
@Slf4j
public class MongoLoopUtil<T> {
private Object loopValue = null;
private String sortKey;
private MongoTemplate mongoTemplate;
private Class<T> returnObj;
private int batchSize;
private String collection;
private String[] excludes;
private int count;
public void setExcludes(String[] excludes) {
this.excludes = excludes;
}
public MongoLoopUtil(
MongoTemplate mongoTemplate,
String sortKey,
Class<T> type,
int batchSize,
String collection) {
this.mongoTemplate = mongoTemplate;
this.sortKey = sortKey;
this.returnObj = type;
this.batchSize = batchSize;
this.collection = collection;
}
public List<T> get(Criteria criteria) {
return get(collection, criteria, null);
}
public List<T> get(Criteria criteria, String[] includeField) {
return get(collection, criteria, includeField);
}
public List<T> get(String collection, Criteria criteria, String[] includeField) {
Query query = new Query();
query.addCriteria(criteria);
query.with(Sort.by(Sort.Order.asc(sortKey)));
if (loopValue != null) {
query.addCriteria(Criteria.where(sortKey).gt(loopValue));
}
if (includeField != null) {
query.fields().include(includeField);
}
if (excludes != null) {
query.fields().exclude(excludes);
}
query.limit(batchSize);
List<T> list = null;
if (collection != null) {
list = mongoTemplate.find(query, returnObj, collection);
} else {
list = mongoTemplate.find(query, returnObj);
}
if (list.size() == 0) {
loopValue = null;
} else {
T objLast = list.get(list.size() - 1);
JSONObject jsonObject = JSONObject.parseObject(JSONObject.toJSONString(objLast));
loopValue = jsonObject.get(sortKey);
count += list.size();
}
log.info("MongoLoopUtil already get count:{},collection", count, collection);
return list;
}
}
使用方法:
java
MongoLoopUtil<JSONObject> mongoLoopUtil =
new MongoLoopUtil<>(
mongoTemplate,
"_id",
JSONObject.class,
100,
"test_table");
while (true) {
List<JSONObject> datas = mongoLoopUtil.get(criteria);
if (null == datas || datas.size() == 0) {
break;
}
//doSomeThing
}
可以通过使用的示例看到,需要遍历的时候创建一个MongoLoopUtil对象,其中的泛型就是返回的数据类型,然后构建方法里面传入mongoTemplate和排序的字段,这里排序的字段是_id,然后传入泛型的class,然后传入每次遍历的数量,这里数量是100,然后传入需要遍历的表名,然后这个对象就创建完成了,然后通过get方法就可以遍历数据了,其中criteria是查询条件,一般来说这个条件是不变的。
游标VS排序遍历对比
使用游标优点:
- 游标逐个返回结果,适用于按需加载数据,减少内存占用。
- 可以在查询过程中即时获取到最新的数据,不受排序影响。
使用游标缺点:
- 如果没有合适的索引支持,可能需要对整个集合进行全表扫描,性能较差。
- 在数据变更较多的情况下,游标可能不稳定,有可能会漏掉或重复某些文档。
使用排序优点:
- 如果可以使用索引进行排序,可以提高查询性能。
- 每次查询的性能消耗是稳定且可预测的。
- 遍历中途可以暂停后重新开始
- 对每次遍历处理数据的时间没有要求
使用排序缺点:
- 需要事先知道排序的字段,并且需要有适当的索引支持。
- 在数据变更较多的情况下,可能需要考虑新数据的插入和旧数据的删除,以确保数据的准确性。
- 复杂查询可能性能不好
总结
总体来说游标遍历和排序遍历各有优缺点,各位还是要根据实际的业务情况去分析选择最合适的遍历方法。