Mongodb遍历数据,游标VS排序(附实现代码)

前言

在使用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排序遍历对比

使用游标优点:

  • 游标逐个返回结果,适用于按需加载数据,减少内存占用。
  • 可以在查询过程中即时获取到最新的数据,不受排序影响。

使用游标缺点:

  • 如果没有合适的索引支持,可能需要对整个集合进行全表扫描,性能较差。
  • 在数据变更较多的情况下,游标可能不稳定,有可能会漏掉或重复某些文档。

使用排序优点:

  • 如果可以使用索引进行排序,可以提高查询性能。
  • 每次查询的性能消耗是稳定且可预测的。
  • 遍历中途可以暂停后重新开始
  • 对每次遍历处理数据的时间没有要求

使用排序缺点:

  • 需要事先知道排序的字段,并且需要有适当的索引支持。
  • 在数据变更较多的情况下,可能需要考虑新数据的插入和旧数据的删除,以确保数据的准确性。
  • 复杂查询可能性能不好

总结

总体来说游标遍历和排序遍历各有优缺点,各位还是要根据实际的业务情况去分析选择最合适的遍历方法。

相关推荐
码上一元2 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
枫叶_v4 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
杜杜的man5 小时前
【go从零单排】Closing Channels通道关闭、Range over Channels
开发语言·后端·golang
java小吕布5 小时前
Java中Properties的使用详解
java·开发语言·后端
2401_857610036 小时前
Spring Boot框架:电商系统的技术优势
java·spring boot·后端
杨哥带你写代码8 小时前
网上商城系统:Spring Boot框架的实现
java·spring boot·后端
camellias_8 小时前
SpringBoot(二十一)SpringBoot自定义CURL请求类
java·spring boot·后端
背水8 小时前
初识Spring
java·后端·spring
晴天飛 雪9 小时前
Spring Boot MySQL 分库分表
spring boot·后端·mysql
weixin_537590459 小时前
《Spring boot从入门到实战》第七章习题答案
数据库·spring boot·后端