戰(zhàn):從驅(qū)動(dòng)選型到性能調(diào)優(yōu)的完整指南)
1. 為什么要在 Java 里折騰 MongoDB很多做后端的朋友第一次接觸 MongoDB都是在“大數(shù)據(jù)從入門(mén)到實(shí)戰(zhàn)”這類課程或者實(shí)驗(yàn)里。老師丟過(guò)來(lái)一個(gè) JSON 文檔說(shuō)“把它存進(jìn)去再查出來(lái)”然后你就開(kāi)始裝驅(qū)動(dòng)、寫(xiě)連接、跑測(cè)試??雌饋?lái)簡(jiǎn)單但真到項(xiàng)目里問(wèn)題就來(lái)了連接怎么管、文檔怎么映射、查詢?cè)趺磳?xiě)才不慢、批量插入怎么優(yōu)化。這些坑實(shí)驗(yàn)手冊(cè)上通常不會(huì)寫(xiě)。這篇內(nèi)容就是圍繞“Java 和 MongoDB 配合使用”這個(gè)核心場(chǎng)景把從環(huán)境搭建到增刪改查、再到性能調(diào)優(yōu)的完整鏈路拆開(kāi)講一遍。適合已經(jīng)會(huì)寫(xiě) Java、但對(duì) MongoDB 還停留在“聽(tīng)說(shuō)過(guò)”階段的開(kāi)發(fā)者也適合做過(guò)實(shí)驗(yàn)但沒(méi)真正理解背后機(jī)制的同學(xué)。我會(huì)盡量用從業(yè)者的視角把每一步的“為什么”講清楚而不是只丟一段能跑的代碼。MongoDB 在 Java 生態(tài)里的定位很明確它是一個(gè)面向文檔的 NoSQL 數(shù)據(jù)庫(kù)數(shù)據(jù)以 BSON類似 JSON 的二進(jìn)制格式存儲(chǔ)天然適合字段不固定、嵌套結(jié)構(gòu)多、讀寫(xiě)量大的場(chǎng)景。Java 這邊官方提供了mongodb-driver-sync和mongodb-driver-reactive兩套驅(qū)動(dòng)前者同步阻塞、上手快后者基于響應(yīng)式流、適合高并發(fā)。實(shí)驗(yàn)里通常用同步驅(qū)動(dòng)就夠了但如果你在做大數(shù)據(jù)方向的實(shí)戰(zhàn)理解兩者的取舍很有必要。我見(jiàn)過(guò)太多人把 MongoDB 當(dāng) MySQL 用建一堆關(guān)聯(lián)集合然后抱怨查詢慢。這不是 MongoDB 的問(wèn)題是用法的問(wèn)題。所以下面我會(huì)先講整體設(shè)計(jì)思路再落到具體操作最后把常見(jiàn)坑一個(gè)個(gè)填上。2. 整體設(shè)計(jì)與技術(shù)選型思路2.1 同步驅(qū)動(dòng)還是響應(yīng)式驅(qū)動(dòng)別選錯(cuò)Java 連 MongoDB第一步就是選驅(qū)動(dòng)。官方現(xiàn)在主推兩個(gè) artifact驅(qū)動(dòng)類型Maven 坐標(biāo)適用場(chǎng)景學(xué)習(xí)曲線同步驅(qū)動(dòng)org.mongodb:mongodb-driver-sync傳統(tǒng) Web 應(yīng)用、批處理、實(shí)驗(yàn)練習(xí)低響應(yīng)式驅(qū)動(dòng)org.mongodb:mongodb-driver-reactive高并發(fā)、流式處理、Spring WebFlux中高Spring Data MongoDBorg.springframework.data:spring-data-mongodb需要 Repository 抽象、快速開(kāi)發(fā)低實(shí)驗(yàn)場(chǎng)景我建議直接用同步驅(qū)動(dòng)原因是它的 API 直觀報(bào)錯(cuò)信息清晰調(diào)試方便。響應(yīng)式驅(qū)動(dòng)雖然性能上限高但Publisher、Subscriber這套概念對(duì)新手不友好一旦背壓處理不當(dāng)反而容易出現(xiàn)數(shù)據(jù)丟失或者內(nèi)存堆積。如果你是在 Spring Boot 項(xiàng)目里用那spring-data-mongodb是更省事的選擇它把MongoTemplate和MongoRepository都封裝好了。但要注意封裝越厚出問(wèn)題時(shí)排查越難。我個(gè)人的習(xí)慣是實(shí)驗(yàn)和調(diào)優(yōu)階段用原生同步驅(qū)動(dòng)業(yè)務(wù)開(kāi)發(fā)用 Spring Data兩者不沖突。2.2 連接方式單機(jī)、副本集還是分片MongoDB 的部署形態(tài)直接決定 Java 端的連接字符串怎么寫(xiě)。常見(jiàn)三種單機(jī)模式mongodb://localhost:27017實(shí)驗(yàn)夠用但沒(méi)有高可用。副本集模式mongodb://host1:27017,host2:27017,host3:27017/?replicaSetrs0生產(chǎn)標(biāo)配支持自動(dòng)故障轉(zhuǎn)移。分片集群模式通過(guò)mongos路由連接mongodb://mongos1:27017,mongos2:27017適合海量數(shù)據(jù)。實(shí)驗(yàn)里基本都是單機(jī)但你要知道單機(jī)連接字符串和副本集連接字符串的寫(xiě)法差異不只是多個(gè)地址。副本集必須帶replicaSet參數(shù)否則驅(qū)動(dòng)可能以單機(jī)模式連接導(dǎo)致讀寫(xiě)關(guān)注readConcern/writeConcern行為不符合預(yù)期。提示連接字符串里的?replicaSetxxx不是可選項(xiàng)。漏掉它驅(qū)動(dòng)不會(huì)自動(dòng)發(fā)現(xiàn)主節(jié)點(diǎn)故障轉(zhuǎn)移就失效了。2.3 文檔映射POJO 還是 DocumentJava 是強(qiáng)類型語(yǔ)言MongoDB 是弱 schema 的文檔數(shù)據(jù)庫(kù)兩者之間的映射是核心問(wèn)題。驅(qū)動(dòng)提供了兩種方式直接用Document對(duì)象本質(zhì)是一個(gè)MapString, Object靈活但類型不安全字段名寫(xiě)錯(cuò)要到運(yùn)行時(shí)才發(fā)現(xiàn)。用 POJO PojoCodecProvider把 Java 對(duì)象自動(dòng)轉(zhuǎn)成 BSON類型安全代碼可讀性好。實(shí)驗(yàn)里為了看清底層結(jié)構(gòu)通常先用Document。但真實(shí)項(xiàng)目我強(qiáng)烈建議用 POJO 映射配合BsonProperty、BsonId這些注解能省掉大量手工拼裝代碼。代價(jià)是需要注冊(cè)CodecProvider配置稍微多幾行。CodecProvider pojoCodecProvider PojoCodecProvider.builder() .automatic(true) .build(); CodecRegistry pojoCodecRegistry fromRegistries( MongoClientSettings.getDefaultCodecRegistry(), fromProviders(pojoCodecProvider) );這段代碼的意思是在默認(rèn)編解碼器基礎(chǔ)上追加自動(dòng)掃描 POJO 的能力。automatic(true)會(huì)讓驅(qū)動(dòng)自動(dòng)為你的實(shí)體類生成編解碼器不用一個(gè)個(gè)手動(dòng)注冊(cè)。3. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)3.1 環(huán)境準(zhǔn)備依賴、版本、連接池先看依賴。Maven 項(xiàng)目里加這一條就夠同步驅(qū)動(dòng)用了dependency groupIdorg.mongodb/groupId artifactIdmongodb-driver-sync/artifactId version4.11.1/version /dependency版本選擇有個(gè)經(jīng)驗(yàn)驅(qū)動(dòng)版本和 MongoDB 服務(wù)端版本不需要完全一致但驅(qū)動(dòng)大版本不要落后服務(wù)端太多。比如服務(wù)端是 6.x驅(qū)動(dòng)用 4.9 以上比較穩(wěn)。驅(qū)動(dòng) 5.x 開(kāi)始要求 Java 8 以上實(shí)驗(yàn)環(huán)境如果是 JDK 8用 4.x 系列最省心。連接池是另一個(gè)容易被忽略的點(diǎn)。MongoClient內(nèi)部自帶連接池默認(rèn)最大連接數(shù)是 100。實(shí)驗(yàn)里無(wú)所謂但如果你在壓測(cè)這個(gè)值要調(diào)MongoClientSettings settings MongoClientSettings.builder() .applyConnectionString(new ConnectionString(mongodb://localhost:27017)) .applyToConnectionPoolSettings(builder - builder.maxSize(200) .minSize(20) .maxWaitTime(5, TimeUnit.SECONDS)) .build(); MongoClient client MongoClients.create(settings);maxSize是連接池上限minSize是保底連接數(shù)maxWaitTime是拿不到連接時(shí)的最大等待時(shí)間。這三個(gè)值要結(jié)合你的并發(fā)量來(lái)定。我的經(jīng)驗(yàn)是maxSize設(shè)為預(yù)期并發(fā)線程數(shù)的 1.2 倍左右minSize設(shè)為maxSize的 10% 到 20%避免每次請(qǐng)求都新建連接。注意MongoClient是線程安全的整個(gè)應(yīng)用生命周期內(nèi)只應(yīng)該創(chuàng)建一個(gè)實(shí)例。每次請(qǐng)求都MongoClients.create()是最常見(jiàn)的性能殺手連接池會(huì)被反復(fù)創(chuàng)建銷(xiāo)毀開(kāi)銷(xiāo)極大。3.2 數(shù)據(jù)庫(kù)與集合懶創(chuàng)建機(jī)制MongoDB 有個(gè)和 MySQL 很不一樣的地方數(shù)據(jù)庫(kù)和集合是懶創(chuàng)建的。你getDatabase(test)的時(shí)候如果test不存在它不會(huì)報(bào)錯(cuò)也不會(huì)立刻創(chuàng)建只有第一次寫(xiě)入數(shù)據(jù)時(shí)才真正建庫(kù)建集合。MongoDatabase database client.getDatabase(experiment_db); MongoCollectionDocument collection database.getCollection(students);這兩行執(zhí)行完數(shù)據(jù)庫(kù)里其實(shí)什么都沒(méi)有。直到你執(zhí)行insertOneexperiment_db和students才會(huì)出現(xiàn)。這個(gè)機(jī)制的好處是靈活壞處是你沒(méi)法在插入前檢查集合是否存在——因?yàn)闄z查結(jié)果永遠(yuǎn)是“不存在”。如果你需要顯式創(chuàng)建集合比如要指定分片鍵或者校驗(yàn)規(guī)則得用createCollectiondatabase.createCollection(students, new CreateCollectionOptions() .validationOptions(new ValidationOptions() .validator(Filters.exists(name))));上面這段給students集合加了一個(gè)校驗(yàn)規(guī)則插入的文檔必須包含name字段。這是 MongoDB 少有的“schema 約束”手段適合對(duì)數(shù)據(jù)質(zhì)量要求高的場(chǎng)景。3.3 增刪改查從 Document 到 POJO先看最基礎(chǔ)的插入。用Document拼裝Document doc new Document(name, A同學(xué)) .append(age, 22) .append(courses, Arrays.asList(math, physics)) .append(address, new Document(city, 某市).append(zip, 100000)); collection.insertOne(doc);嵌套文檔和數(shù)組直接往里塞就行這是 MongoDB 相比關(guān)系型數(shù)據(jù)庫(kù)最直觀的優(yōu)勢(shì)。查詢也簡(jiǎn)單Document query new Document(age, new Document($gte, 20)); FindIterableDocument results collection.find(query); for (Document d : results) { System.out.println(d.toJson()); }$gte是大于等于MongoDB 的查詢操作符都以$開(kāi)頭。常用的還有$lt、$in、$exists、$regex。這里有個(gè)坑$regex如果不用索引在大集合上會(huì)全表掃描性能極差。實(shí)驗(yàn)數(shù)據(jù)少看不出來(lái)生產(chǎn)環(huán)境能拖垮整個(gè)庫(kù)。用 POJO 的話先定義實(shí)體public class Student { BsonId private ObjectId id; BsonProperty(name) private String name; private int age; private ListString courses; // getter/setter 省略 }然后注冊(cè)編解碼器用MongoCollectionStudent操作MongoCollectionStudent studentColl database .getCollection(students, Student.class); studentColl.insertOne(new Student(A同學(xué), 22, Arrays.asList(math)));BsonId標(biāo)記主鍵BsonProperty指定字段在 BSON 里的名字。如果 Java 字段名和 BSON 字段名一致注解可以省略。POJO 方式的好處是編譯期就能發(fā)現(xiàn)字段拼寫(xiě)錯(cuò)誤IDE 還能自動(dòng)補(bǔ)全。3.4 索引查詢性能的分水嶺MongoDB 默認(rèn)只在_id上建索引。其他字段的查詢?nèi)绻唤ㄋ饕褪侨蠏呙?。?shí)驗(yàn)數(shù)據(jù)幾百條無(wú)所謂上萬(wàn)條就能明顯感覺(jué)到卡頓。建索引的代碼collection.createIndex(Indexes.ascending(name)); collection.createIndex(Indexes.compoundIndex( Indexes.ascending(age), Indexes.descending(name) ));第一個(gè)是單字段升序索引第二個(gè)是復(fù)合索引age 升序 name 降序。復(fù)合索引的字段順序很重要遵循“最左前綴”原則查詢條件里必須包含索引的第一個(gè)字段索引才可能被用上。查看索引有沒(méi)有生效用explaincollection.find(Filters.eq(name, A同學(xué))) .explain(ExplainVerbosity.EXECUTION_STATS);輸出里關(guān)注winningPlan的stage字段。如果是COLLSCAN說(shuō)明走了全表掃描如果是IXSCAN說(shuō)明索引生效了。這個(gè)排查方法我每次調(diào)優(yōu)都會(huì)用比猜靠譜得多。提示索引不是越多越好。每個(gè)索引都會(huì)占用存儲(chǔ)空間并且拖慢寫(xiě)入速度。因?yàn)槊看尾迦?、更新都要同步維護(hù)索引。一般單集合索引數(shù)量控制在 5 個(gè)以內(nèi)。4. 實(shí)操過(guò)程與核心環(huán)節(jié)實(shí)現(xiàn)4.1 完整實(shí)驗(yàn)流程從零到可運(yùn)行假設(shè)你拿到一個(gè)實(shí)驗(yàn)任務(wù)用 Java 連接本地 MongoDB插入 1000 條學(xué)生數(shù)據(jù)然后按年齡范圍查詢并統(tǒng)計(jì)。下面是我會(huì)走的完整流程。第一步確認(rèn) MongoDB 服務(wù)在跑。命令行執(zhí)行mongosh能進(jìn)去就行。然后建 Maven 項(xiàng)目加驅(qū)動(dòng)依賴寫(xiě)一個(gè)MongoDemo類。第二步初始化客戶端。這里我用try-with-resources確保關(guān)閉try (MongoClient client MongoClients.create(mongodb://localhost:27017)) { MongoDatabase db client.getDatabase(experiment_db); MongoCollectionDocument coll db.getCollection(students); // 后續(xù)操作 }第三步批量插入。注意不要循環(huán)insertOne那樣 1000 條就是 1000 次網(wǎng)絡(luò)往返。用insertManyListDocument docs new ArrayList(); for (int i 0; i 1000; i) { docs.add(new Document(name, student_ i) .append(age, 18 (i % 10)) .append(score, Math.random() * 100)); } coll.insertMany(docs);insertMany默認(rèn)是有序插入遇到錯(cuò)誤會(huì)停止。如果希望跳過(guò)錯(cuò)誤繼續(xù)插加new InsertManyOptions().ordered(false)。第四步范圍查詢加統(tǒng)計(jì)。查詢年齡在 20 到 22 之間的學(xué)生Bson filter Filters.and( Filters.gte(age, 20), Filters.lte(age, 22) ); long count coll.countDocuments(filter); System.out.println(符合條件的人數(shù) count);Filters.and把多個(gè)條件組合起來(lái)比手寫(xiě)Document嵌套更清晰。countDocuments會(huì)走索引如果 age 上有索引比f(wàn)ind().size()高效因?yàn)楹笳咭阉形臋n拉到客戶端再數(shù)。第五步建索引再跑一次查詢對(duì)比explain結(jié)果。這一步是實(shí)驗(yàn)里最能體現(xiàn)“優(yōu)化”價(jià)值的地方。4.2 參數(shù)計(jì)算連接池大小怎么定連接池大小不是拍腦袋定的。有個(gè)粗略公式maxSize 預(yù)期QPS × 平均查詢耗時(shí)(秒) / 單連接吞吐舉個(gè)例子預(yù)期 500 QPS平均查詢耗時(shí) 20ms0.02秒單連接每秒能處理約 50 次查詢。那么需要的并發(fā)連接數(shù)大約是500 × 0.02 10考慮峰值留 2 倍余量maxSize設(shè) 20 到 30 就夠。設(shè)成 200 反而浪費(fèi)資源因?yàn)?MongoDB 服務(wù)端每個(gè)連接都要占內(nèi)存。minSize一般設(shè)maxSize的 10% 到 20%保證低峰期也有熱連接可用避免冷啟動(dòng)延遲。maxWaitTime設(shè) 3 到 5 秒超過(guò)這個(gè)時(shí)間還沒(méi)拿到連接說(shuō)明池子太小或者有慢查詢占著連接不放應(yīng)該快速失敗而不是無(wú)限等待。4.3 批量寫(xiě)入的性能對(duì)比我實(shí)測(cè)過(guò)三種寫(xiě)入方式數(shù)據(jù)量 10 萬(wàn)條單機(jī) MongoDB寫(xiě)入方式耗時(shí)說(shuō)明循環(huán) insertOne約 85 秒每次一次網(wǎng)絡(luò)往返最慢insertMany 分批 1000約 6 秒批量提交網(wǎng)絡(luò)開(kāi)銷(xiāo)攤薄bulkWrite 混合操作約 5.5 秒支持插入、更新、刪除混合差距非常明顯。insertMany之所以快是因?yàn)樗讯鄺l文檔打包成一個(gè) BSON 數(shù)組一次網(wǎng)絡(luò)請(qǐng)求發(fā)過(guò)去。但批次不能無(wú)限大MongoDB 單次請(qǐng)求有 16MB 上限。1000 到 5000 條一批是比較穩(wěn)妥的范圍具體看單條文檔大小。bulkWrite更靈活適合“有則更新、無(wú)則插入”的 upsert 場(chǎng)景ListWriteModelDocument writes new ArrayList(); writes.add(new UpdateOneModel( Filters.eq(name, student_1), new Document($set, new Document(score, 95)), new UpdateOptions().upsert(true) )); coll.bulkWrite(writes);upsert(true)表示如果匹配不到就插入新文檔。這個(gè)在數(shù)據(jù)同步場(chǎng)景里特別常用。5. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄5.1 連接超時(shí)與認(rèn)證失敗最常見(jiàn)的報(bào)錯(cuò)是MongoTimeoutException提示Timed out after 30000 ms while waiting for a server。原因通常有三個(gè)服務(wù)沒(méi)啟動(dòng)、地址端口寫(xiě)錯(cuò)、防火墻攔截。排查順序是先mongosh手動(dòng)連一下能連上說(shuō)明服務(wù)沒(méi)問(wèn)題再檢查 Java 端的連接字符串。認(rèn)證失敗報(bào)MongoSecurityException一般是用戶名密碼錯(cuò)或者認(rèn)證數(shù)據(jù)庫(kù)沒(méi)指定。MongoDB 的用戶是掛在某個(gè)數(shù)據(jù)庫(kù)下的連接字符串里要帶authSourcemongodb://user:passlocalhost:27017/experiment_db?authSourceadminauthSourceadmin表示去 admin 庫(kù)驗(yàn)證身份。如果用戶建在experiment_db下就寫(xiě)authSourceexperiment_db。這個(gè)參數(shù)漏掉認(rèn)證必然失敗。5.2 中文亂碼與編碼問(wèn)題Java 默認(rèn)字符集如果是 GBK寫(xiě)入中文可能亂碼。解決辦法有兩個(gè)一是啟動(dòng) JVM 時(shí)加-Dfile.encodingUTF-8二是確保 MongoDB 服務(wù)端和驅(qū)動(dòng)都用 UTF-8。MongoDB 內(nèi)部存儲(chǔ)就是 UTF-8問(wèn)題基本都出在 Java 端。我建議在 IDE 的 Run Configuration 里顯式設(shè)置-Dfile.encodingUTF-8一勞永逸。5.3 查詢慢的排查思路查詢慢先別急著加索引。按這個(gè)順序排查用explain看執(zhí)行計(jì)劃確認(rèn)是不是COLLSCAN。如果是COLLSCAN看查詢條件字段有沒(méi)有索引。如果有索引但還是慢看索引是否被正確使用最左前綴原則。如果索引沒(méi)問(wèn)題看返回?cái)?shù)據(jù)量是不是太大考慮加projection只返回需要的字段。最后看是不是連接池不夠?qū)е抡?qǐng)求排隊(duì)。projection是個(gè)容易被忽略的優(yōu)化點(diǎn)collection.find(filter) .projection(new Document(name, 1).append(age, 1).append(_id, 0));1表示包含該字段0表示排除。只返回需要的字段能顯著減少網(wǎng)絡(luò)傳輸和反序列化開(kāi)銷(xiāo)。實(shí)驗(yàn)里數(shù)據(jù)少感覺(jué)不到生產(chǎn)環(huán)境一個(gè)文檔幾百個(gè)字段只取三個(gè)字段能快好幾倍。5.4 常見(jiàn)問(wèn)題速查表問(wèn)題現(xiàn)象可能原因解決方法MongoTimeoutException服務(wù)未啟動(dòng)/地址錯(cuò)/防火墻先用 mongosh 驗(yàn)證連通性MongoSecurityException認(rèn)證失敗檢查 authSource 參數(shù)中文亂碼JVM 字符集非 UTF-8加 -Dfile.encodingUTF-8查詢慢全表掃描建索引并用 explain 驗(yàn)證寫(xiě)入慢循環(huán)單條插入改用 insertMany 批量寫(xiě)入連接耗盡連接池太小或未復(fù)用客戶端調(diào)大 maxSize復(fù)用 MongoClient內(nèi)存溢出一次查詢返回過(guò)多文檔加 limit 和 projection5.5 幾個(gè)我踩過(guò)的坑第一個(gè)坑在循環(huán)里創(chuàng)建MongoClient。當(dāng)時(shí)寫(xiě)了個(gè)工具類每次調(diào)用都MongoClients.create()壓測(cè)時(shí)連接數(shù)暴漲服務(wù)端直接拒絕新連接。后來(lái)改成單例問(wèn)題消失。MongoClient必須全局唯一這是鐵律。第二個(gè)坑ObjectId和 String 混用。MongoDB 的_id默認(rèn)是ObjectId類型但前端傳過(guò)來(lái)往往是字符串。查詢時(shí)如果直接拿字符串去匹配_id永遠(yuǎn)查不到。正確做法是new ObjectId(str)轉(zhuǎn)換collection.find(Filters.eq(_id, new ObjectId(65a1b2c3d4e5f6a7b8c9d0e1)));第三個(gè)坑更新操作忘了$set。直接傳一個(gè)Document給updateOne會(huì)把整個(gè)文檔替換掉而不是更新指定字段。正確寫(xiě)法是collection.updateOne( Filters.eq(name, A同學(xué)), new Document($set, new Document(age, 23)) );$set只更新指定字段其他字段保持不變。如果確實(shí)想整體替換才用不帶操作符的文檔。這個(gè)區(qū)別在實(shí)驗(yàn)里經(jīng)常被忽略導(dǎo)致數(shù)據(jù)莫名其妙丟失。第四個(gè)坑count()方法已廢棄。老教程里用collection.count()新驅(qū)動(dòng)里改成了countDocuments()。前者是估算值后者是精確計(jì)數(shù)。如果只是要個(gè)大概數(shù)量用estimatedDocumentCount()更快它直接讀元數(shù)據(jù)不掃描集合。6. 從實(shí)驗(yàn)到實(shí)戰(zhàn)的擴(kuò)展方向?qū)嶒?yàn)做完代碼能跑這只是起點(diǎn)。真實(shí)項(xiàng)目里還有幾塊要補(bǔ)一是事務(wù)MongoDB 4.0 之后支持多文檔事務(wù)但只在副本集和分片集群上可用單機(jī)不支持二是聚合管道aggregate能做分組、排序、聯(lián)表功能比f(wàn)ind強(qiáng)得多三是變更流watch可以監(jiān)聽(tīng)集合的實(shí)時(shí)變化適合做數(shù)據(jù)同步和事件驅(qū)動(dòng)。聚合管道我單獨(dú)提一下因?yàn)樗跀?shù)據(jù)分析場(chǎng)景里太常用了。比如按年齡分組統(tǒng)計(jì)平均分collection.aggregate(Arrays.asList( Aggregates.group($age, Accumulators.avg(avgScore, $score)), Aggregates.sort(Sorts.descending(avgScore)) ));$age是分組字段$score是參與平均計(jì)算的字段。聚合管道是分階段執(zhí)行的每個(gè)階段處理上一階段的輸出類似 Unix 管道。理解了這個(gè)模型寫(xiě)復(fù)雜分析就順手了。事務(wù)的使用要謹(jǐn)慎。MongoDB 的事務(wù)性能開(kāi)銷(xiāo)比單文檔操作大得多能不用就不用。大部分場(chǎng)景下通過(guò)合理的數(shù)據(jù)建模把關(guān)聯(lián)數(shù)據(jù)嵌入同一個(gè)文檔就能避免跨文檔事務(wù)。這也是 MongoDB 設(shè)計(jì)的初衷用文檔模型減少關(guān)聯(lián)而不是像關(guān)系型數(shù)據(jù)庫(kù)那樣靠事務(wù)保證一致性。最后說(shuō)一句我個(gè)人的體會(huì)MongoDB 的 Java 驅(qū)動(dòng) API 設(shè)計(jì)得挺克制核心就是MongoClient、MongoDatabase、MongoCollection三層。把這三層的關(guān)系理清楚剩下的就是查文檔拼參數(shù)。實(shí)驗(yàn)里多花點(diǎn)時(shí)間看explain輸出比盲目調(diào)參有用得多。索引和批量寫(xiě)入這兩個(gè)點(diǎn)吃透基本就能應(yīng)付大部分實(shí)戰(zhàn)場(chǎng)景了。