首页 > 头条 >

快看点丨mongodb 深度分页优化思路之cursor游标

2023-06-23 15:05:31 来源:博客园 分享到 :

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

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

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


(资料图片)

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

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

实践步骤:

1. 引入mongo 驱动:

                    org.mongodb            mongodb-driver-sync            4.4.2                            org.mongodb            mongodb-driver-core            4.4.2                            org.mongodb            bson            4.4.2        

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

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 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 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;@Servicepublic class MongoDbService {    private MongoClient mongoClient;    // 所有游标容器,简单测试,真正的管理很复杂    private Map> 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 findDataWithCursor(String searchAfter, int limit) {        ensureMongo();        MongoDatabase database = mongoClient.getDatabase("local");        MongoCollection collection = database.getCollection("test01");        List resultList = new ArrayList<>();        MongoCursor 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,绝对是个超级复杂的事,何时打开,何时关闭,超时处理,机器宕机等,很难解决。

推荐阅读