mongodb 深度分页优化思路之cursor游标

发布时间 2023-06-23 15:17:57作者: 等你归去来

  mongodb 没有官方的游标滚动实现深度分页功能,建议的都是选择出一个字段,如_id,然后每次查询时限制该字段,而不进行分页处理。

  也没有看到更优的实现方式,本文做一个大胆的假设,自行实现滚动分页功能。供大家思路参考。

  但是猜想可以自行实现一个,简单思路就是,第一次查询时不带limit进行查询全量数据,然后自己通过cursor迭代出需要的行数后返回调用端,下次再调用时,直接取出上一次的cursor,再迭代limit的数量返回。

  优势是只需计算一次,后续就直接复用结果即可。该功能需要有mongodb的clientSession功能支持。

  但是需要复杂的自己维护cursor实例,打开、关闭、过期等。稍微管理不好,可能就客户端内存泄漏或者mongo server内存泄漏。

 

实践步骤:

1. 引入mongo 驱动:

        <!-- https://mvnrepository.com/artifact/org.mongodb/mongodb-driver-sync -->
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongodb-driver-sync</artifactId>
            <version>4.4.2</version>
        </dependency>
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongodb-driver-core</artifactId>
            <version>4.4.2</version>
        </dependency>
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>bson</artifactId>
            <version>4.4.2</version>
        </dependency>

  注意版本不匹配问题,所以要引入多个包。

 

2. 创建测试类:

  验证接入mongo无误,且造入适量的数据。

import static com.mongodb.client.model.Filters.eq;

import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.WriteConcern;
import com.mongodb.client.*;
import com.mongodb.client.result.InsertOneResult;
import org.bson.Document;
import org.junit.Before;
import org.junit.Test;
import org.openjdk.jmh.annotations.Setup;

public class MongoQuickStartTest {

    private MongoClient mongoClient;

    @Before
    public void setup() {
        // Replace the placeholder with your MongoDB deployment's connection string
        String uri = "mongodb://localhost:27017";
        MongoClientSettings options = MongoClientSettings.builder()
                .applyConnectionString(new ConnectionString(uri))
                .writeConcern(WriteConcern.W1).build();
        mongoClient = MongoClients.create(options);
    }

    @Test
    public void testFind() {
//        ConnectionString connectionString = new ConnectionString("mongodb://localhost:27017");
//        MongoClient mongoClient = MongoClients.create(connectionString);
        // Replace the placeholder with your MongoDB deployment's connection string
        MongoDatabase database = mongoClient.getDatabase("local");
        MongoCollection<Document> collection = database.getCollection("test01");
        Document doc = collection.find(eq("name", "zhangsan1")).first();
        if (doc != null) {
            System.out.println(doc.toJson());
        } else {
            System.out.println("No matching documents found.");
        }
    }

    @Test
    public void testInsert() {
        Document body = new Document();
        long startId = 60011122212L;
        MongoDatabase database = mongoClient.getDatabase("local");
        MongoCollection<Document> collection = database.getCollection("test01");
        int i;
        for (i = 0; i < 500000; i++) {
            String id = (startId + i) + "";
            body.put("_id", id);
            body.put("name", "name_" + id);
            body.put("title", "title_" + id);
            InsertOneResult result = collection.insertOne(body);
        }
        System.out.println("insert " + i + " rows");
    }
}

 

3. 创建cursor的查询实现类并调用

  基于springboot创建 controller进行会话测试,使用一个固定的查询语句进行分页测试。

import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.WriteConcern;
import com.mongodb.client.*;
import org.bson.Document;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class MongoDbService {

    private MongoClient mongoClient;
    // 所有游标容器,简单测试,真正的管理很复杂
    private Map<String, MongoCursor<Document>> cursorHolder
            = new ConcurrentHashMap<>();

    public void ensureMongo() {
        // Replace the placeholder with your MongoDB deployment's connection string
        String uri = "mongodb://localhost:27017";
        MongoClientSettings options = MongoClientSettings.builder()
                .applyConnectionString(new ConnectionString(uri))
                .writeConcern(WriteConcern.W1).build();
        mongoClient = MongoClients.create(options);
    }

    // 特殊实现的 cursor 滚动查询
    public List<Document> findDataWithCursor(String searchAfter, int limit) {
        ensureMongo();
        MongoDatabase database = mongoClient.getDatabase("local");
        MongoCollection<Document> collection = database.getCollection("test01");
        List<Document> resultList = new ArrayList<>();
        MongoCursor<Document> cursor = cursorHolder.get(searchAfter);
        if(cursor == null) {
            // 第一次取用需要查询,后续直接复用cursor即可
            cursor = collection.find().sort(new Document("name", 1)).iterator();
            cursorHolder.put(searchAfter, cursor);
        }
        int i = 0;
        // 自行计数,到达后即返回前端
        while (cursor.hasNext()) {
            resultList.add(cursor.next());
            if(++i >= limit) {
                break;
            }
        }
        if(!cursor.hasNext()) {
            cursor.close();
            cursorHolder.remove(searchAfter);
        }
        return resultList;
    }
}

  应用调用controller:

    @Resource
    private MongoDbService mongoDbService;

    @GetMapping("/mongoPageScroll")
    @ResponseBody
    public Object mongoPageScroll(@RequestParam(required = false) String params,
                                  @RequestParam String scrollId) {
        return mongoDbService.findDataWithCursor(scrollId, 9);
    }

  测试方式,访问接口:http://localhost:8080/hello/mongoPageScroll?scrollId=c,然后反复调用(下一页)。

  如此,只要前端第一次查询时,不存在cursor就创建,后续就直接使用原来的结果。第一次可能慢,第二次就很快了。

  结论,是可以简单实现的,但是生产不一定能用。因为,如何管理cursor,绝对是个超级复杂的事,何时打开,何时关闭,超时处理,机器宕机等,很难解决。