前言
在使用mongodb的時(shí)候,經(jīng)常會(huì)有這樣的業(yè)務(wù)場(chǎng)景薪夕,比如搜索某個(gè)條件脚草,然后這個(gè)條件的結(jié)果有幾十萬甚至幾百萬,然后一時(shí)半會(huì)處理不過來原献,就需要使用遍歷循環(huán)來處理馏慨。一般來說遍歷大量的數(shù)據(jù)有三種方法:
- 第一種就是用mongodb自帶的游標(biāo)去遍歷
- 第二種是用排序然后取最后一個(gè)id去遍歷
- 第三種是使用limit和skip去遍歷
當(dāng)數(shù)據(jù)量很少的時(shí)候可以使用第三種方法遍歷,其他時(shí)候均不適合使用第三種方法遍歷姑隅。本文主要對(duì)比第一種和第二種方法的優(yōu)劣
使用游標(biāo)遍歷
一般來說直接使用mongodb的find查詢写隶,會(huì)返回一個(gè)游標(biāo),默認(rèn)是返回20條讲仰,使用游標(biāo)的next()方法可以繼續(xù)訪問下一頁腥放,類似一個(gè)翻頁器赊时。但是要注意,不要輕易的去調(diào)用游標(biāo)的toArray()方法,除非你在確定返回結(jié)果數(shù)量的情況下荠雕,否則游標(biāo)會(huì)把所有數(shù)據(jù)加載到內(nèi)存。游標(biāo)可以通過batchSize來設(shè)置每頁的數(shù)量
游標(biāo)需要注意的地方
首先瓮具,游標(biāo)是一個(gè)內(nèi)存的狀態(tài)槽袄,在默認(rèn)配置下,一個(gè)游標(biāo)在兩次getmore間隔超過10分鐘毫捣,那么這個(gè)游標(biāo)就會(huì)被回收详拙,也就是說在批量處理數(shù)據(jù)的時(shí)候,如果發(fā)生卡頓或者執(zhí)行時(shí)間超過預(yù)期蔓同,就有可能導(dǎo)致當(dāng)前游標(biāo)被回收饶辙,然后無法繼續(xù)遍歷,報(bào)錯(cuò)找不到游標(biāo)牌柄。當(dāng)然可以調(diào)整這個(gè)延遲時(shí)間或者縮小批量的數(shù)量來避免這個(gè)問題
其次畸悬,游標(biāo)的本質(zhì)是數(shù)據(jù)庫的一個(gè)指針,指向了數(shù)據(jù)的地址珊佣,所以當(dāng)數(shù)據(jù)發(fā)生變化的時(shí)候蹋宦,可能會(huì)出現(xiàn)混亂的情況。
游標(biāo)的返回是不保證順序的咒锻,如果使用排序冷冗,會(huì)占用大量的資源。同時(shí)因?yàn)椴槐WC順序的情況惑艇,遍歷是無法暫停后繼續(xù)的蒿辙。
示例代碼
首先導(dǎo)入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);
// 執(zhí)行查詢拇泛,獲取游標(biāo)
MongoCursor<Document> cursor = collection.find().iterator();
// 遍歷游標(biāo)
while (cursor.hasNext()) {
Document document = cursor.next();
// 將 Document 轉(zhuǎn)換為 JSONObject
JSONObject jsonObject = new JSONObject(document.toJson());
// 處理每個(gè) JSONObject
}
// 關(guān)閉游標(biāo)
cursor.close();
}
使用排序遍歷
一般來說,排序遍歷是使用某個(gè)唯一字段作為排序來遍歷思灌,每次都取結(jié)果的最后一個(gè)數(shù)據(jù)的這個(gè)字段來作為下一次查詢的條件俺叭,使用limit來控制性能。比如:通過_id來遍歷一個(gè)數(shù)據(jù)集合泰偿,先使用limit(100)拿到100條數(shù)據(jù)熄守,然后取最后一個(gè)數(shù)據(jù)的_id假設(shè)為idA,然后在下一次遍歷的時(shí)候加入條件 {"_id":{$gt:idA}然后繼續(xù)limit(100),以此類推,來達(dá)到遍歷的效果
排序遍歷需要注意的地方
排序遍歷每次都會(huì)使用排序耗跛,當(dāng)條件很簡(jiǎn)單或者是遍歷所有數(shù)據(jù)的時(shí)候裕照,這種方法是性能和準(zhǔn)確性的最佳方法,同時(shí)每次遍歷數(shù)據(jù)消耗的性能都是比較平均的调塌,不容易造成數(shù)據(jù)庫性能擁堵晋南。
排序遍歷在條件比較復(fù)雜的情況下,性能可能受索引的影響羔砾,在條件很多的情況下负间,排序遍歷挺難所有的查詢都使用索引,特別是_id排序姜凄,往往后面的遍歷只會(huì)使用的到_id的索引唉擂。所以條件復(fù)雜的時(shí)候需要測(cè)試性能來避免遍歷引起過多數(shù)據(jù)庫開銷。
排序遍歷的java實(shí)現(xiàn)
以springboot來說檀葛,以下是排序遍歷的一個(gè)java工具玩祟,大家可以直接復(fù)制使用
@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
}
可以通過使用的示例看到,需要遍歷的時(shí)候創(chuàng)建一個(gè)MongoLoopUtil對(duì)象屿聋,其中的泛型就是返回的數(shù)據(jù)類型空扎,然后構(gòu)建方法里面?zhèn)魅雖ongoTemplate和排序的字段,這里排序的字段是_id润讥,然后傳入泛型的class转锈,然后傳入每次遍歷的數(shù)量,這里數(shù)量是100楚殿,然后傳入需要遍歷的表名撮慨,然后這個(gè)對(duì)象就創(chuàng)建完成了,然后通過get方法就可以遍歷數(shù)據(jù)了脆粥,其中criteria是查詢條件砌溺,一般來說這個(gè)條件是不變的。
游標(biāo)VS排序遍歷對(duì)比
使用游標(biāo)優(yōu)點(diǎn):
- 游標(biāo)逐個(gè)返回結(jié)果变隔,適用于按需加載數(shù)據(jù)规伐,減少內(nèi)存占用。
- 可以在查詢過程中即時(shí)獲取到最新的數(shù)據(jù)匣缘,不受排序影響猖闪。
使用游標(biāo)缺點(diǎn):
- 如果沒有合適的索引支持鲜棠,可能需要對(duì)整個(gè)集合進(jìn)行全表掃描,性能較差培慌。
- 在數(shù)據(jù)變更較多的情況下豁陆,游標(biāo)可能不穩(wěn)定,有可能會(huì)漏掉或重復(fù)某些文檔吵护。
使用排序優(yōu)點(diǎn):
- 如果可以使用索引進(jìn)行排序献联,可以提高查詢性能。
- 每次查詢的性能消耗是穩(wěn)定且可預(yù)測(cè)的何址。
- 遍歷中途可以暫停后重新開始
- 對(duì)每次遍歷處理數(shù)據(jù)的時(shí)間沒有要求
使用排序缺點(diǎn):
- 需要事先知道排序的字段,并且需要有適當(dāng)?shù)乃饕С帧?/li>
- 在數(shù)據(jù)變更較多的情況下进胯,可能需要考慮新數(shù)據(jù)的插入和舊數(shù)據(jù)的刪除用爪,以確保數(shù)據(jù)的準(zhǔn)確性。
- 復(fù)雜查詢可能性能不好
總結(jié)
總體來說游標(biāo)遍歷和排序遍歷各有優(yōu)缺點(diǎn)胁镐,各位還是要根據(jù)實(shí)際的業(yè)務(wù)情況去分析選擇最合適的遍歷方法偎血。