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

前言

在使用mongodb的时候,经常会有这样的业务场景,比如搜索某个条件,然后这个条件的结果有几十万甚至几百万,然后一时半会处理不过来,就需要使用遍历循环来处理。一般来说遍历大量的数据有三种方法:

  • 第一种就是用mongodb自带的游标去遍历
  • 第二种是用排序然后取最后一个id去遍历
  • 第三种是使用limit和skip去遍历

当数据量很少的时候可以使用第三种方法遍历,其他时候均不适合使用第三种方法遍历。本文主要对比第一种和第二种方法的优劣

使用游标遍历

一般来说直接使用mongodb的find查询,会返回一个游标,默认是返回20条,使用游标的next()方法可以继续访问下一页,类似一个翻页器。但是要注意,不要轻易的去调用游标的toArray()方法,除非你在确定返回结果数量的情况下,否则游标会把所有数据加载到内存。游标可以通过batchSize来设置每页的数量

游标需要注意的地方

首先,游标是一个内存的状态,在默认配置下,一个游标在两次getmore间隔超过10分钟,那么这个游标就会被回收,也就是说在批量处理数据的时候,如果发生卡顿或者执行时间超过预期,就有可能导致当前游标被回收,然后无法继续遍历,报错找不到游标。当然可以调整这个延迟时间或者缩小批量的数量来避免这个问题
其次,游标的本质是数据库的一个指针,指向了数据的地址,所以当数据发生变化的时候,可能会出现混乱的情况。
游标的返回是不保证顺序的,如果使用排序,会占用大量的资源。同时因为不保证顺序的情况,遍历是无法暂停后继续的。

示例代码

首先导入maven依赖

 <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>

然后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工具,大家可以直接复制使用

@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;
    }
}

使用方法:

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

使用游标优点:

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

使用游标缺点:

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

使用排序优点:

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

使用排序缺点:

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

总结

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容