MongoDB中cursor.toArray()性能过慢问题排查与优化咨询
我在使用cursor.toArray()将collection.find(query)的结果转换为列表时,API响应时间长达数百毫秒,但实际返回的数据量极少(仅数百条记录)。数据库已为查询字段ZIP建立索引,我也设置了cursor.batchSize(1000),查询语句db.collection.find({"ZIP" : {"$in" : ["12345"]}})在Mongo Shell中执行仅需4-5毫秒。我使用的Mongo驱动为Mongojack 2.8.2,相关代码如下:
依赖配置
<!-- https://mvnrepository.com/artifact/org.mongojack/mongojack --> <dependency> <groupId>org.mongojack</groupId> <artifactId>mongojack</artifactId> <version>2.8.2</version> </dependency>
资源类代码
@Path("/") @Produces(MediaType.APPLICATION_JSON) @Api(value = "listing-mongo") public class MlsMongoResource { private JacksonDBCollection<Mlsdatadao, String> collection; Clock clock; public MlsMongoResource(JacksonDBCollection<Mlsdatadao, String> collection) { this.collection = collection; this.clock = Clock.systemUTC(); } @GET @Path("/listings-mongo") @Produces(value = MediaType.APPLICATION_JSON) @Timed public List<Mlsdatadao> getListings(@BeanParam MlsListingParameters mlsBeanParam) { BasicDBList basicDbList = new BasicDBList(); mlsBeanParam.validateBean(); setLocations(basicDbList,mlsBeanParam.zipcodes); BasicDBObject query = new BasicDBObject("$and", basicDbList); DBCursor<Mlsdatadao> cursor = null; long start = 0; try{ start = System.currentTimeMillis(); cursor = collection.find(query); cursor.batchSize(1000); } catch (Exception e){ System.out.println("IN collection.find() " + e.getCause()); } System.out.println("QUERY LIST IS " + basicDbList); if(cursor == null) { System.out.println("Cursor is null"); } List<Mlsdatadao> result = cursor.toArray(); cursor.close(); System.out.println(System.currentTimeMillis() - start); return result; } private void setLocations(BasicDBList basicDbList, List<String> zipcodes) { if (CollectionUtils.isNotEmpty(zipcodes)) { basicDbList.add(setZipcodes(zipcodes)); } } private BasicDBObject setZipcodes(List<String> zipcodes) { return new BasicDBObject("ZIP" , new BasicDBObject("$in", zipcodes) ); } }
应用启动代码
public class MongoApplication extends Application <MlsMongoConfiguration> { public static void main(String[] args) throws Exception { new MlsMongoApplication().run(args); } @Override public String getName() { return "mls-dropwizard-mongo"; } @Override public void initialize(Bootstrap<MlsMongoConfiguration> bootstrap) { bootstrap.addBundle(new SwaggerBundle<MlsMongoConfiguration>() { @Override protected SwaggerBundleConfiguration getSwaggerBundleConfiguration(MlsMongoConfiguration configuration) { return configuration.swaggerBundleConfiguration; } }); } @Override public void run(MlsMongoConfiguration configuration, Environment environment) throws Exception { MongoClientOptions.Builder clientOptions = new MongoClientOptions.Builder(); clientOptions.minConnectionsPerHost(1000);//min clientOptions.maxWaitTime(1000); clientOptions.connectionsPerHost(1000); //Create Mongo instance //Mongo mongo = new Mongo(configuration.mongohost, configuration.mongoport); MongoClient mongoClient = new MongoClient(new ServerAddress(configuration.mongohost, configuration.mongoport), clientOptions.build()); //Add Managed for managing the Mongo instance //MongoManaged mongoManaged = new MongoManaged(mongo); MongoManaged mongoManaged = new MongoManaged(mongoClient); environment.lifecycle().manage(mongoManaged); //Add Health check for Mongo instance. This will be used from the Health check admin page environment.healthChecks().register("MongoHealthCheck", new MongoHealthCheck(mongoClient)); //Create DB instance and wrap it in a Jackson DB collection DB db = mongoClient.getDB(configuration.mongodb); JacksonDBCollection<Mlsdatadao, String> jacksonDBCollection = JacksonDBCollection.wrap(db.getCollection("mlsdata"), Mlsdatadao.class, String.class); environment.jersey().register(new MlsMongoResource(jacksonDBCollection)); } }
请问是否有办法避免使用cursor.toArray()?其他性能调优建议也非常感谢。
一、避免cursor.toArray()的替代方案
cursor.toArray()会一次性把所有查询结果加载到内存中,哪怕数据量不大,也会产生额外的内存拷贝和对象初始化开销。这里给你两个更优的替代方式:
- 手动遍历游标构建结果列表
不用依赖toArray(),直接遍历游标把元素逐个加入列表,能减少toArray()内部的数组扩容等隐性开销,实际测试中往往能小幅缩短响应时间:
List<Mlsdatadao> result = new ArrayList<>(); if (cursor != null) { while (cursor.hasNext()) { result.add(cursor.next()); } }
- 流式返回响应(适合大结果集或低延迟需求)
如果你的客户端支持流式JSON解析,可以直接用Jersey的StreamingOutput来输出结果,这样不用把所有数据加载到内存,客户端能边接收边处理,大幅降低内存占用和响应延迟:
@GET @Path("/listings-mongo") @Produces(MediaType.APPLICATION_JSON) @Timed public StreamingOutput getListings(@BeanParam MlsListingParameters mlsBeanParam) { BasicDBList basicDbList = new BasicDBList(); mlsBeanParam.validateBean(); setLocations(basicDbList, mlsBeanParam.zipcodes); BasicDBObject query = new BasicDBObject("$and", basicDbList); DBCursor<Mlsdatadao> cursor = collection.find(query).batchSize(1000); return outputStream -> { ObjectMapper mapper = new ObjectMapper(); try { mapper.writeValue(outputStream, cursor); } catch (IOException e) { throw new WebApplicationException("Failed to stream data", e); } finally { if (cursor != null) { cursor.close(); } } }; }
注意这种方式需要确保客户端能处理流式输出,同时一定要在finally块里关闭游标,避免资源泄漏。
二、其他性能调优建议
1. 优化Mongojack的对象映射效率
Mongojack的对象转换是常见的性能瓶颈点,你可以试试这些优化:
- 给
Mlsdatadao类的字段添加@JsonProperty注解,明确和Mongo文档字段的映射关系,减少反射时的字段查找开销; - 添加
@JsonIgnoreProperties(ignoreUnknown = true)注解,忽略文档中不需要的字段,避免不必要的映射处理; - 临时换成Mongo原生
Document对象获取数据,对比响应时间,如果耗时大幅减少,说明问题出在对象映射环节,再针对性优化。
2. 调整Mongo客户端连接配置
你当前的客户端配置有些参数不太合理,建议调整:
maxWaitTime(1000)设置过小,数据库临时负载高时容易触发连接超时,建议改成30000(30秒);connectionsPerHost(1000)和minConnectionsPerHost(1000)过高,除非你的QPS极高,否则大量空闲连接会浪费资源,建议改成connectionsPerHost(100)、minConnectionsPerHost(10);- 添加
socketTimeout(30000)和connectTimeout(10000),避免读写或连接时无限等待。
3. 确认查询真的用上了索引
虽然你说建了ZIP索引,但最好用explain()验证查询计划:
在Mongo Shell执行:
db.collection.find({"ZIP" : {"$in" : ["12345"]}}).explain("executionStats")
重点看这两个指标:
executionStats.totalDocsExamined应该等于返回的文档数,否则说明扫描了多余的文档;executionStages.inputStage.stage如果是IXSCAN说明用到了索引,如果是COLLSCAN则没用到,可能是索引字段类型不匹配(比如数据库里ZIP是数字,你查的是字符串)或者索引未正确创建。
4. 提前设置batchSize
你当前是先获取游标再设置batchSize,建议改成链式调用,确保batchSize在查询执行前就生效:
cursor = collection.find(query).batchSize(1000);
虽然Mongojack可能会处理后续设置,但链式调用更稳妥,避免游标已开始获取数据后设置不生效的情况。
5. 减少日志输出开销
代码里的System.out.println在生产环境会带来IO开销,建议换成SLF4J日志框架,并且把日志级别调到INFO或更高,避免DEBUG级别的日志拖慢响应速度。
内容的提问来源于stack exchange,提问作者sudarshan kakumanu

