返回文章列表

MongoDB 更新操作與交易效能最佳化

本文探討 MongoDB 更新操作和交易處理的效能最佳化技巧,涵蓋使用聚合框架、Upsert 操作、Bulk Upsert with $merge 以及交易重試成本和最佳化策略,幫助開發者提升 MongoDB 應用程式的效能。

資料函式庫 效能調校

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 } }
    );
});

內容解密:

  1. 檢索資料:使用 db.customers.find() 檢索所有客戶資料。
  2. 迴圈更新:對每筆客戶資料執行更新操作,設定 viewCountviews 陣列的長度。
  3. 效能問題:這種方法需要大量的資料傳輸和多次更新操作,效能較差。

MongoDB 4.2 引入了聚合框架管道(Aggregation Pipeline),允許在更新陳述式中嵌入聚合管道,從而提高效能。以下是一個範例:

db.customers.update(
    {},
    [{ $set: { viewCount: { $size: '$views' } } }],
    { multi: true }
);

內容解密:

  1. 單一更新陳述式:使用一個更新陳述式更新所有檔案。
  2. 聚合框架:使用 $set 聚合運算子和 $size 聚合運算子計算 views 陣列的大小。
  3. 效能提升:這種方法大幅減少了執行時間,約為原來的 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;
});

內容解密:

  1. 檢查是否存在:檢查目標集合中是否存在匹配的檔案。
  2. 更新或插入:如果存在,則更新;否則,插入新的檔案。
  3. 效能提升:Upsert 操作減少了需要執行的資料函式庫命令數量,從而提高了效能。

Bulk Upsert with $merge

MongoDB 4.2 引入了 $merge 聚合運算子,允許批次執行 Upsert 操作。以下是一個範例:

db.source.aggregate([{
    $merge: {
        into: "target",
        on: "_id",
        whenMatched: "replace",
        whenNotMatched: "insert"
    }
}]);

內容解密:

  1. 批次處理:使用聚合框架批次處理資料。
  2. $merge 運算子:根據 _id 欄位匹配目標集合中的檔案,如果匹配,則替換;否則,插入新的檔案。
  3. 效能提升:這種方法避免了資料在網路中的傳輸,並且減少了需要執行的 MongoDB 陳述式數量,從而大幅提高了效能。

刪除操作的最佳化

刪除操作需要更新集合中的所有索引,因此對於具有大量索引的集合,刪除操作可能會成為效能瓶頸。最佳化刪除操作的方法包括減少索引數量和使用批次刪除操作。

交易處理最佳化

在 MongoDB 中,交易(Transactions)是維護資料一致性和正確性的重要機制,尤其是在多使用者平行操作的情況下。雖然交易能提升資料的一致性,但也會降低平行度,從而影響資料函式庫效能。本章將重點探討如何最大化交易吞吐量和最小化交易等待時間。

交易理論基礎

資料函式庫通常採用兩種主要的架構模式來滿足一致性需求:ACID 交易和多版本平行控制(MVCC)。

ACID 交易模型

ACID 交易模型是在 1980 年代開發的,具有以下四個特性:

  • 原子性(Atomic):交易是不可分割的,要麼所有陳述式都應用於資料函式庫,要麼都不應用。
  • 一致性(Consistent):資料函式庫在交易執行前後保持一致狀態。
  • 隔離性(Isolated):多個交易可以同時執行,但一個交易不應看到其他進行中的交易的影響。
  • 永續性(Durable):一旦交易被提交,其變更應持久儲存,即使發生作業系統或硬體故障。

實作 ACID 一致性最簡單的方法是使用鎖定機制。然而,根據鎖定的一致性可能導致過高的競爭和低平行度。

多版本平行控制(MVCC)

現代資料函式庫系統幾乎普遍採用 MVCC 模型,以在不增加過多鎖定的情況下提供 ACID 一致性。在 MVCC 模型中,多個資料副本被標記時間戳或變更識別符號,使資料函式庫能夠在給定時間點構建快照。

此圖示說明瞭 MVCC 的運作方式:

@startuml
note
  無法自動轉換的 Plantuml 圖表
  請手動檢查和調整
@enduml

MVCC 模型運作流程

  1. 資料函式庫會話 1 在時間 t1 開始一個交易。
  2. 在時間 t2,會話 1 更新一個檔案,資料函式庫建立該檔案的新版本。
  3. 同時,會話 2 查詢該檔案,由於會話 1 的交易尚未提交,會話 2 看到的是舊版本的檔案。
  4. 在時間 t3,會話 1 提交交易。
  5. 在時間 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)存在兩個主要差異:

  1. 多版本平行控制(MVCC)機制:在 MongoDB 4.4 之前,MongoDB 並未在磁碟上維護多個版本的資料區塊來支援 MVCC。相反,它將資料區塊儲存在 WiredTiger 快取記憶體中。這種設計導致 MongoDB 的交易時間限制預設為 60 秒,以避免對 WiredTiger 記憶體造成過大壓力。從 MongoDB 4.4 開始,Snapshot 資料可以寫入磁碟,但預設的交易時間限制仍為 60 秒。

  2. 衝突處理機制:與傳統 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();

內容解密:

  1. 建立兩個會話(session1session2)並開始各自的交易。
  2. session1Collectionsession2Collection 對同一個檔案(_id: 1)進行更新操作。
  3. session2Collection 嘗試更新已被 session1Collection 修改的檔案時,MongoDB 發出 TransientTransactionError
  4. 程式需要處理此錯誤並重試 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);
    }
}

內容解密:

  1. myTransaction 函式在指定的會話中執行一個交易,包含兩個更新操作:從一個帳戶轉出資金到另一個帳戶。
  2. session.withTransaction 自動處理交易邏輯,包括在發生 TransientTransactionError 時的重試機制。
  3. 如果發生錯誤,錯誤訊息會被記錄到控制檯。

重試機制的實作與日誌記錄

雖然 MongoDB Drivers 自動處理 TransientTransactionErrors,但這些錯誤仍然由 MongoDB 伺服器發出,並記錄在 MongoDB 日誌中。可以透過日誌過濾或驅動程式提供的除錯功能來檢視這些重試操作。

程式碼範例(NodeJS 除錯日誌):

// 在 NodeJS Driver 中啟用除錯日誌以檢視底層的重試操作

內容解密:

  1. 在 NodeJS Driver 中,可以啟用除錯日誌來觀察底層的交易重試操作。
  2. 當交易因 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 用於取得交易相關的統計資訊。
  • totalStartedtotalAbortedtotalCommitted 分別記錄了交易的啟動、終止和提交次數。
  • 計算終止交易佔總啟動交易的百分比,以評估交易的成功率。

交易最佳化策略

為了減少 TransientTransactionError 的重試次數,可以採用以下幾種策略:

  1. 避免使用交易:在某些情況下,可以透過重新設計資料模型來避免使用交易。例如,將相關資料儲存在單一檔案中,以原子操作更新資料。

  2. 最佳化交易中的操作順序:將最可能引起寫入衝突的操作放在交易的末尾,以減少衝突的視窗期。

  3. 分割「熱點」檔案:如果某些檔案頻繁被更新,可以考慮將這些檔案分割,以減少寫入衝突。

範例:避免使用交易

假設有一個銀行應用,需要在兩個分支之間轉帳。使用交易可以確保兩個更新操作要麼同時成功,要麼同時失敗。然而,如果分支數量較少,可以將所有分支的餘額儲存在單一檔案的嵌入式陣列中,並使用單一更新操作來完成轉帳。

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);

內容解密:

  • 將更新全域計數器的操作放在交易的最後,減少了寫入衝突的可能性。
  • 這種順序的最佳化可以顯著提升交易的執行效率。