MongoDB 的效能對於資料密集型應用至關重要。本文探討如何最佳化 MongoDB 的更新操作和交易處理,以提升應用程式效率。更新操作方面,我們將探討如何利用聚合框架簡化更新流程,並使用 Upsert 和 Bulk Upsert with $merge 進行高效的資料修改。此外,文章還會詳細介紹 MongoDB 交易的內部機制,包括 MVCC 和衝突處理,並提供減少交易重試次數的實用策略,例如避免不必要的交易、調整操作順序以及分割熱點檔案,以最大程度地減少效能瓶頸。
MongoDB 更新操作的效能最佳化
在處理大量資料時,MongoDB 的更新操作效能至關重要。本文將探討幾種提升更新操作效能的方法,包括使用聚合框架、Upsert 操作以及 Bulk Upsert with $merge。
使用聚合框架最佳化更新操作
在 MongoDB 4.2 之前,更新操作需要先檢索資料,然後再進行更新,這樣會導致大量的資料在網路中傳輸,並且需要執行多個更新陳述式。例如,以下程式碼用於更新客戶資料的 viewCount 欄位:
db.customers.find().forEach(customer => {
let updRC = db.customers.update(
{ _id: customer['_id'] },
{ $set: { viewCount: customer.views.length } }
);
});
內容解密:
- 檢索資料:使用
db.customers.find()檢索所有客戶資料。 - 迴圈更新:對每筆客戶資料執行更新操作,設定
viewCount為views陣列的長度。 - 效能問題:這種方法需要大量的資料傳輸和多次更新操作,效能較差。
MongoDB 4.2 引入了聚合框架管道(Aggregation Pipeline),允許在更新陳述式中嵌入聚合管道,從而提高效能。以下是一個範例:
db.customers.update(
{},
[{ $set: { viewCount: { $size: '$views' } } }],
{ multi: true }
);
內容解密:
- 單一更新陳述式:使用一個更新陳述式更新所有檔案。
- 聚合框架:使用
$set聚合運算子和$size聚合運算子計算views陣列的大小。 - 效能提升:這種方法大幅減少了執行時間,約為原來的 5%。
使用 Upsert 操作
Upsert 操作允許在單一陳述式中執行更新或插入操作,從而提高效能。以下是一個範例:
db.source.find().forEach(doc => {
let returnCodes = db.target.update({ _id: doc['_id'] }, doc, { upsert: true });
inserts += returnCodes.nUpserted;
updates += returnCodes.nModified;
});
內容解密:
- 檢查是否存在:檢查目標集合中是否存在匹配的檔案。
- 更新或插入:如果存在,則更新;否則,插入新的檔案。
- 效能提升:Upsert 操作減少了需要執行的資料函式庫命令數量,從而提高了效能。
Bulk Upsert with $merge
MongoDB 4.2 引入了 $merge 聚合運算子,允許批次執行 Upsert 操作。以下是一個範例:
db.source.aggregate([{
$merge: {
into: "target",
on: "_id",
whenMatched: "replace",
whenNotMatched: "insert"
}
}]);
內容解密:
- 批次處理:使用聚合框架批次處理資料。
- $merge 運算子:根據
_id欄位匹配目標集合中的檔案,如果匹配,則替換;否則,插入新的檔案。 - 效能提升:這種方法避免了資料在網路中的傳輸,並且減少了需要執行的 MongoDB 陳述式數量,從而大幅提高了效能。
刪除操作的最佳化
刪除操作需要更新集合中的所有索引,因此對於具有大量索引的集合,刪除操作可能會成為效能瓶頸。最佳化刪除操作的方法包括減少索引數量和使用批次刪除操作。
交易處理最佳化
在 MongoDB 中,交易(Transactions)是維護資料一致性和正確性的重要機制,尤其是在多使用者平行操作的情況下。雖然交易能提升資料的一致性,但也會降低平行度,從而影響資料函式庫效能。本章將重點探討如何最大化交易吞吐量和最小化交易等待時間。
交易理論基礎
資料函式庫通常採用兩種主要的架構模式來滿足一致性需求:ACID 交易和多版本平行控制(MVCC)。
ACID 交易模型
ACID 交易模型是在 1980 年代開發的,具有以下四個特性:
- 原子性(Atomic):交易是不可分割的,要麼所有陳述式都應用於資料函式庫,要麼都不應用。
- 一致性(Consistent):資料函式庫在交易執行前後保持一致狀態。
- 隔離性(Isolated):多個交易可以同時執行,但一個交易不應看到其他進行中的交易的影響。
- 永續性(Durable):一旦交易被提交,其變更應持久儲存,即使發生作業系統或硬體故障。
實作 ACID 一致性最簡單的方法是使用鎖定機制。然而,根據鎖定的一致性可能導致過高的競爭和低平行度。
多版本平行控制(MVCC)
現代資料函式庫系統幾乎普遍採用 MVCC 模型,以在不增加過多鎖定的情況下提供 ACID 一致性。在 MVCC 模型中,多個資料副本被標記時間戳或變更識別符號,使資料函式庫能夠在給定時間點構建快照。
此圖示說明瞭 MVCC 的運作方式:
@startuml
note
無法自動轉換的 Plantuml 圖表
請手動檢查和調整
@enduml
MVCC 模型運作流程
- 資料函式庫會話 1 在時間 t1 開始一個交易。
- 在時間 t2,會話 1 更新一個檔案,資料函式庫建立該檔案的新版本。
- 同時,會話 2 查詢該檔案,由於會話 1 的交易尚未提交,會話 2 看到的是舊版本的檔案。
- 在時間 t3,會話 1 提交交易。
- 在時間 t4,會話 2 再次查詢該檔案,此時看到的是更新後的新版本。
交易最佳化實踐
為了最大化交易吞吐量和最小化交易等待時間,請遵循以下最佳實踐:
- 盡可能減少交易中的運算元量和複雜度。
- 避免在交易中進行不必要的查詢或更新操作。
- 使用適當的索引來加速交易中的查詢操作。
- 監控交易效能,找出瓶頸並進行最佳化。
程式碼最佳化範例
假設我們有一個更新使用者餘額的交易:
db.users.updateOne({ _id: userId }, { $inc: { balance: amount } });
為了最佳化這個交易,我們可以新增適當的索引:
db.users.createIndex({ _id: 1 });
程式碼解密
db.users.createIndex({ _id: 1 });:在users集合的_id欄位上建立升序索引,以加速根據_id查詢或更新的操作。- 索引可以顯著提高查詢效能,從而減少交易的執行時間。
MongoDB 交易處理機制深度解析
MongoDB 的交易(Transactions)功能與傳統的 SQL 資料函式庫交易在實作上有顯著差異。瞭解這些差異對於有效使用 MongoDB 的交易功能至關重要。
MongoDB 交易與 SQL 資料函式庫交易的差異
MongoDB 的交易實作與傳統 SQL 資料函式庫(如 MySQL、PostgreSQL)存在兩個主要差異:
多版本平行控制(MVCC)機制:在 MongoDB 4.4 之前,MongoDB 並未在磁碟上維護多個版本的資料區塊來支援 MVCC。相反,它將資料區塊儲存在 WiredTiger 快取記憶體中。這種設計導致 MongoDB 的交易時間限制預設為 60 秒,以避免對 WiredTiger 記憶體造成過大壓力。從 MongoDB 4.4 開始,Snapshot 資料可以寫入磁碟,但預設的交易時間限制仍為 60 秒。
衝突處理機制:與傳統 SQL 資料函式庫使用鎖定機制不同,MongoDB 不使用阻塞鎖來防止交易之間的衝突。相反,當可能發生衝突時,MongoDB 會發出
TransientTransactionError以終止可能引起衝突的交易。
交易限制
MongoDB 使用 MVCC 機制(如圖 9-1 所示)來確保交易看到獨立且一致的資料函式庫表示。這種快照隔離(Snapshot Isolation)確保交易看到一致的資料檢視,並且會話不會觀察到未提交的交易。MongoDB 的這種隔離機制被稱為快照讀取關注(Snapshot Read Concern)。
大多數實作 MVCC 的關聯式資料函式庫使用根據磁碟的「before image」或「rollback」段來儲存建立資料函式庫快照所需的資料。在這些資料函式庫中,快照的「年齡」僅受限於磁碟上的可用磁碟空間量。
然而,MongoDB 的初始實作依賴於儲存在 WiredTiger 記憶體快取中的資料副本。因此,MongoDB 無法可靠地維護長時間執行的交易的資料快照。為了避免對 WiredTiger 記憶體造成壓力,交易預設限制為 60 秒。可以透過修改 transactionLifetimeLimitSeconds 引數來更改此限制。
TransientTransactionErrors 處理
大多數關聯式資料函式庫(如 PostgreSQL 或 MySQL)使用鎖定來實作交易一致性。當一個會話修改表中的某一列時,它會對該列放置鎖定,以防止平行修改。如果第二個會話嘗試修改同一列,則必須等待直到原始交易提交後鎖定被釋放。
許多開發者熟悉關聯式資料函式庫的阻塞鎖定,並可能假設 MongoDB 也採用相同的做法。然而,MongoDB 的做法完全不同。在 MongoDB 中,當第二個會話嘗試修改在另一個交易中已修改的檔案時,它不會等待鎖定被釋放。相反,它會收到 TransientTransactionError 事件。第二個會話必須重試該交易(理想情況下在第一個交易完成後)。
程式碼範例:
var session1 = db.getMongo().startSession();
var session2 = db.getMongo().startSession();
var session1Collection = session1.getDatabase(db.getName()).transTest;
var session2Collection = session2.getDatabase(db.getName()).transTest;
session1.startTransaction();
session2.startTransaction();
session1Collection.update({_id: 1}, {$set: {value: 1}});
session2Collection.update({_id: 1}, {$set: {value: 2}});
session1.commitTransaction();
session2.commitTransaction();
內容解密:
- 建立兩個會話(
session1和session2)並開始各自的交易。 session1Collection和session2Collection對同一個檔案(_id: 1)進行更新操作。- 當
session2Collection嘗試更新已被session1Collection修改的檔案時,MongoDB 發出TransientTransactionError。 - 程式需要處理此錯誤並重試
session2的交易。
MongoDB Drivers 中的交易處理
從 MongoDB 4.2 開始,MongoDB Drivers 自動隱藏 TransientTransactionError,並自動重試交易。例如,在 NodeJS 中,可以執行多個此類別程式碼副本,而不會遇到任何 TransientTransactionErrors:
async function myTransaction(session, db, fromAcc, toAcc, dollars) {
try {
await session.withTransaction(async () => {
await db.collection('accounts').updateOne({ _id: fromAcc }, { $inc: { balance: -1 * dollars } }, { session });
await db.collection('accounts').updateOne({ _id: toAcc }, { $inc: { balance: dollars } }, { session });
}, transactionOptions);
} catch (error) {
console.log(error.message);
}
}
內容解密:
myTransaction函式在指定的會話中執行一個交易,包含兩個更新操作:從一個帳戶轉出資金到另一個帳戶。session.withTransaction自動處理交易邏輯,包括在發生TransientTransactionError時的重試機制。- 如果發生錯誤,錯誤訊息會被記錄到控制檯。
重試機制的實作與日誌記錄
雖然 MongoDB Drivers 自動處理 TransientTransactionErrors,但這些錯誤仍然由 MongoDB 伺服器發出,並記錄在 MongoDB 日誌中。可以透過日誌過濾或驅動程式提供的除錯功能來檢視這些重試操作。
程式碼範例(NodeJS 除錯日誌):
// 在 NodeJS Driver 中啟用除錯日誌以檢視底層的重試操作
內容解密:
- 在 NodeJS Driver 中,可以啟用除錯日誌來觀察底層的交易重試操作。
- 當交易因
TransientTransactionError而中止時,日誌中會顯示相關的中止命令。
MongoDB 交易效能最佳化:減少重試以提升效率
MongoDB 的交易(Transaction)功能為開發者提供了在多個操作中保持資料一致性的能力。然而,交易過程中可能出現的 TransientTransactionError 會導致重試,進而影響效能。本篇文章將探討如何最佳化 MongoDB 交易,以減少重試次數並提升整體效能。
交易重試的成本
當 TransientTransactionError 發生時,MongoDB 會自動重試交易。這個過程不僅涉及丟棄已完成的工作,還需要將資料函式庫狀態回復到交易開始前的狀態。這些操作都是昂貴的,會顯著影響交易效能。
監控交易狀態
要了解交易的執行情況,可以使用 db.serverStatus().transactions 來檢查交易的啟動、終止和提交次數。以下是一個範例函式,用於列印交易相關的統計資訊:
function txnCounts() {
var ssTxns = db.serverStatus().transactions;
print(ssTxns.totalStarted + 0, 'transactions started');
print(ssTxns.totalAborted + 0, 'transactions aborted');
print(ssTxns.totalCommitted + 0, 'transactions committed');
print(Math.round(ssTxns.totalAborted * 100 / ssTxns.totalStarted) + '% txns aborted');
}
內容解密:
db.serverStatus().transactions用於取得交易相關的統計資訊。totalStarted、totalAborted和totalCommitted分別記錄了交易的啟動、終止和提交次數。- 計算終止交易佔總啟動交易的百分比,以評估交易的成功率。
交易最佳化策略
為了減少 TransientTransactionError 的重試次數,可以採用以下幾種策略:
避免使用交易:在某些情況下,可以透過重新設計資料模型來避免使用交易。例如,將相關資料儲存在單一檔案中,以原子操作更新資料。
最佳化交易中的操作順序:將最可能引起寫入衝突的操作放在交易的末尾,以減少衝突的視窗期。
分割「熱點」檔案:如果某些檔案頻繁被更新,可以考慮將這些檔案分割,以減少寫入衝突。
範例:避免使用交易
假設有一個銀行應用,需要在兩個分支之間轉帳。使用交易可以確保兩個更新操作要麼同時成功,要麼同時失敗。然而,如果分支數量較少,可以將所有分支的餘額儲存在單一檔案的嵌入式陣列中,並使用單一更新操作來完成轉帳。
try {
let updateString = `{"$inc":{"branchTotals.` + fromBranch + `.balance":` + (-1 * dollars) + `,"branchTotals.` + toBranch + `.balance":` + dollars + `}}`;
let updateClause = JSON.parse(updateString);
await db.collection('embeddedBranches').updateOne({ _id: 1 }, updateClause);
} catch (error) {
console.log(error.message);
}
內容解密:
- 使用
$inc運算子原子地更新嵌入式陣列中的分支餘額。 - 這種方法避免了使用交易,從而減少了
TransientTransactionError的可能性。 - 將多個操作合併為一個更新操作,大幅提升了效能。
範例:最佳化交易中的操作順序
考慮一個轉帳交易,先更新一個全域的交易計數器,然後更新兩個帳戶的餘額。由於多個交易會競爭更新計數器,這可能導致大量的 TransientTransactionError。
await session.withTransaction(async () => {
await db.collection('accounts').updateOne({ _id: fromAcc }, { $inc: { balance: -1 * dollars } }, { session });
await db.collection('accounts').updateOne({ _id: toAcc }, { $inc: { balance: dollars } }, { session });
await db.collection('txnTotals').updateOne({ _id: 1 }, { $inc: { counter: 1 } }, { session });
}, transactionOptions);
內容解密:
- 將更新全域計數器的操作放在交易的最後,減少了寫入衝突的可能性。
- 這種順序的最佳化可以顯著提升交易的執行效率。