超越邊界:MongoDB 16MB 文檔限制的 pragmatic 解決方案

在軟件開發中,我們選擇的技術棧往往帶有一些固有的設計邊界。對于 MongoDB 而言,其最著名的邊界之一便是 BSON 文檔最大 16MB 的大小限制。在大多數場景下,這個限制是綽綽有余的,它鼓勵開發者設計更為精簡和規范的數據模型。然而,在某些特定業務場景下,單個邏輯實體的數據量自然增長并最終觸及這個上限,就成了一個棘手的問題。本文將分享一次處理此類問題的完整過程,從一個有隱患的初始設計,到一個健壯、可擴展的最終方案。

最初的困境:一個“定時炸彈”式的數據模型

問題的起源是一個看似合理的數據模型。系統中存在一個核心實體,其文檔結構中包含了一個用于聚合數據的集合字段。例如,一個用于記錄和合并分析結果的文檔,其結構可以被簡化為如下形式:

// 初始數據模型
public class ParentDocument {private ObjectId id;private String name;// 該字段用于持續聚合數據,是問題的根源private List<DataObject> aggregatedData;
}

其業務邏輯是,當新的數據片段產生時,系統會讀取整個 ParentDocument,將新的數據片段與 aggregatedData 列表在內存中合并,然后將包含完整新列表的 ParentDocument 整個保存回數據庫。在系統初期,數據量不大,這種“讀取-修改-寫回”的模式運行良好,邏輯清晰且易于實現。

然而,這個方案的弊端是致命的。隨著業務的持續運行,aggregatedData 列表不斷膨脹。每一次合并都使得文檔的體積越來越大,像一個被不斷吹氣的氣球,最終必然會達到其物理極限。當某次合并后的文檔總體積超過 16MB 時,MongoDB 的驅動程序在嘗試保存時會立即拋出 BsonMaximumSizeExceededException 異常,導致整個業務流程中斷。這個問題就像一顆預先埋下的定時炸彈,它的爆炸只是時間問題。

報錯:

error=Payload document size is larger than maximum of 16777216.
org.bson.BsonMaximumSizeExceededException: Payload document size is larger than maximum of 16777216.at com.mongodb.internal.connection.BsonWriterHelper.writePayload(BsonWriterHelper.java:68)at com.mongodb.internal.connection.CommandMessage.encodeMessageBodyWithMetadata(CommandMessage.java:162)at com.mongodb.internal.connection.RequestMessage.encode(RequestMessage.java:138)at com.mongodb.internal.connection.CommandMessage.encode(CommandMessage.java:59)at com.mongodb.internal.connection.InternalStreamConnection.sendAndReceive(InternalStreamConnection.java:268)at com.mongodb.internal.connection.UsageTrackingInternalConnection.sendAndReceive(UsageTrackingInternalConnection.java:100)at com.mongodb.internal.connection.DefaultConnectionPool$PooledConnection.sendAndReceive(DefaultConnectionPool.java:490)at com.mongodb.internal.connection.CommandProtocolImpl.execute(CommandProtocolImpl.java:71)at com.mongodb.internal.connection.DefaultServer$DefaultServerProtocolExecutor.execute(DefaultServer.java:253)at com.mongodb.internal.connection.DefaultServerConnection.executeProtocol(DefaultServerConnection.java:202)at com.mongodb.internal.connection.DefaultServerConnection.command(DefaultServerConnection.java:118)at com.mongodb.internal.operation.MixedBulkWriteOperation.executeCommand(MixedBulkWriteOperation.java:431)at com.mongodb.internal.operation.MixedBulkWriteOperation.executeBulkWriteBatch(MixedBulkWriteOperation.java:251)at com.mongodb.internal.operation.MixedBulkWriteOperation.access$700(MixedBulkWriteOperation.java:76)at com.mongodb.internal.operation.MixedBulkWriteOperation$1.call(MixedBulkWriteOperation.java:194)at com.mongodb.internal.operation.MixedBulkWriteOperation$1.call(MixedBulkWriteOperation.java:185)at com.mongodb.internal.operation.OperationHelper.withReleasableConnection(OperationHelper.java:621)at com.mongodb.internal.operation.MixedBulkWriteOperation.execute(MixedBulkWriteOperation.java:185)at com.mongodb.internal.operation.MixedBulkWriteOperation.execute(MixedBulkWriteOperation.java:76)at com.mongodb.client.internal.MongoClientDelegate$DelegateOperationExecutor.execute(MongoClientDelegate.java:187)at com.mongodb.client.internal.MongoCollectionImpl.executeSingleWriteRequest(MongoCollectionImpl.java:1009)at com.mongodb.client.internal.MongoCollectionImpl.executeInsertOne(MongoCollectionImpl.java:470)at com.mongodb.client.internal.MongoCollectionImpl.insertOne(MongoCollectionImpl.java:453)at com.mongodb.client.internal.MongoCollectionImpl.insertOne(MongoCollectionImpl.java:447)

解決方案的演進:從內嵌聚合到引用分塊

要解決這個問題,核心思想必須從“如何將一個大文檔存進去”轉變為“如何將一個邏輯上的大實體拆分成多個物理上的小文檔”。我們最終采用的方案是“文檔引用分塊”。

這個方案的第一步是重構數據模型。我們將聚合數據從主文檔中剝離出來,存放到一個專門的“數據塊”集合中。主文檔僅保留對這些數據塊的引用。

// 重構后的主文檔模型
public class ParentDocument {private ObjectId id;private String name;// 存儲指向數據塊文檔的ID列表private List<ObjectId> chunkIds; 
}// 新增的數據塊文檔模型
@Document(collection = "dataChunks")
public class ChunkDocument {private ObjectId id;// 每個塊包含一部分數據private List<DataObject> dataSlice;
}

伴隨著數據模型的演進,核心的存取邏輯也需要重構。

在寫入數據時,我們在服務層引入了分塊邏輯。當需要保存一個聚合了大量數據的邏輯實體時,代碼不再直接構建一個巨大的 ParentDocument。取而代之的是:

  1. 判斷大小:評估待保存的總數據量是否超過預設的安全閾值。

  2. 執行分塊:如果超過閾值,則在內存中將龐大的數據列表分割成多個較小的列表。

  3. 持久化塊:遍歷這些小列表,將每一個都包裝成一個 ChunkDocument 并存入 dataChunks 集合。

  4. 保存引用:收集所有新創建的 ChunkDocument_id,將這個 ObjectId 列表存入 ParentDocumentchunkIds 字段中,同時清空其內嵌的數據字段。

  5. 更新清理:在更新一個已存在的 ParentDocument 時,先根據其舊的 chunkIds 刪除所有關聯的舊數據塊,避免產生孤立的垃圾數據。

讀取數據的邏輯則確保了這種底層變化對上層業務的透明性。當業務需要獲取一個完整的 ParentDocument 實體時,數據服務層的邏輯如下:

  1. 獲取主文檔:首先根據ID獲取 ParentDocument

  2. 檢查分塊:判斷其 chunkIds 字段是否有效。

  3. 按需重組:如果 chunkIds 存在,則根據ID列表到 dataChunks 集合中查詢出所有的關聯數據塊。隨后,在內存中將所有 dataSlice 合并成一個完整的數據列表。

  4. 返回完整視圖:將重組后的完整數據列表設置到 ParentDocument 實例的對應字段上(通常是一個非持久化的瞬態字段),再將其轉換為業務DTO(數據傳輸對象)返回。

通過這種方式,無論底層數據是否被分塊,上層業務邏輯得到的永遠是一個數據完整的、與原始設計中一致的邏輯實體。這不僅解決了16MB的限制,也保證了方案的向后兼容性和對其他業務模塊的最小侵入性。

替代方案的思考:為何不是 GridFS?

在探討解決方案時,我們自然會想到 MongoDB 官方提供的大文件存儲方案——GridFS。GridFS 能將任意大小的文件分割成默認255KB的塊進行存儲,非常適合存放圖片、視頻或大型二進制文件。

然而,經過審慎評估,我們認為 GridFS 并不適用于我們當前的業務場景。主要原因在于我們的數據特性和操作模式:

首先,我們的“大文件”并非一個不可分割的二進制“大對象”(BLOB),而是一個由成千上萬個獨立結構化對象(DataObject)組成的集合。我們需要對這個集合進行頻繁的、增量式的合并操作。

若采用 GridFS,每次合并都需要將整個幾十甚至上百兆的邏輯對象從 GridFS 下載到內存,反序列化成一個巨大的列表,與新數據合并后,再將這個全新的、更大的對象序列化并重新上傳到 GridFS,最后刪除舊的 GridFS 文件。這種“整體讀-整體寫”的操作模式對于我們增量更新的場景而言,性能開銷和資源消耗是無法接受的。

其次,GridFS 的設計初衷是文件存儲,我們無法對存儲在其中的內容進行查詢。而我們的“引用分塊”方案,每一個數據塊本身仍然是一個標準的 MongoDB 文檔,保留了未來對部分數據集進行直接查詢的可能性。

最后,引入 GridFS 會導致數據訪問模式的根本性改變,從操作文檔對象變為操作文件流(InputStream),這將對現有的數據服務層、業務邏輯乃至DTO產生巨大的沖擊,與我們期望的“最小化、高內聚”的重構目標背道而馳。

總結

面對看似難以逾越的技術邊界,選擇最“官方”或最“強大”的工具未必是最佳答案。通過深入分析業務數據的結構特性和操作模式,我們最終選擇的“文檔引用分塊”方案,雖然需要自行實現分塊和重組邏輯,但它以一種高度兼容且對業務侵入最小的方式,優雅地解決了16MB的文檔大小限制。這個過程也再次印證了一個樸素的道理:最合適的方案,永遠源于對問題本質的深刻理解。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/91866.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/91866.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/91866.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

深入探討:PostgreSQL正則表達式中的郵政編碼匹配

引言 在處理大量數據時,如何高效地從字符串中提取特定模式的文本,如郵政編碼,是一個常見且具有挑戰性的任務。本文將通過一個具體實例,探討在PostgreSQL中使用正則表達式匹配加拿大郵政編碼的問題,并提供解決方案。 問題描述 我們希望能夠從字符串中提取所有符合加拿大…

集合框架(重點)

第十五天集合框架1.什么是集合 Collections集合Collection&#xff0c;也是一個數據容器&#xff0c;類似于數組&#xff0c;但是和數組是不一樣的。集合是一個可變的容器&#xff0c;可以隨時向集合中添加元素&#xff0c;也可以隨時從集合中刪除元素。另外&#xff0c;集合還…

深度學習核心:神經網絡-激活函數 - 原理、實現及在醫學影像領域的應用

&#x1f9d1; 博主簡介&#xff1a;CSDN博客專家、CSDN平臺優質創作者&#xff0c;高級開發工程師&#xff0c;數學專業&#xff0c;10年以上C/C, C#,Java等多種編程語言開發經驗&#xff0c;擁有高級工程師證書&#xff1b;擅長C/C、C#等開發語言&#xff0c;熟悉Java常用開發…

OneCode3.0 核心表達式技術深度剖析:從架構設計到動態擴展

一、引言&#xff1a;表達式技術在企業級框架中的核心價值 在當今快速變化的企業級應用開發中&#xff0c;動態性和靈活性已成為衡量框架優劣的關鍵指標。OneCode 3.0 框架作為企業級應用開發的重要工具&#xff0c;其核心表達式技術提供了一種強大的解決方案&#xff0c;使開發…

[css]旋轉流光效果

實現一個矩形的旋轉流光邊框效果。 需要使用css屬性梯度漸變&#xff1a;鏈接: conic-gradient&#xff0c;他指的是圓錐形變化的梯度。 // html<div class"demo"></div>// css body {width: 100%;height: 100%;background-color: black; }.demo {width…

NPM組件 @0xme5war/apicli 等竊取主機敏感信息

【高危】NPM組件 0xme5war/apicli 等竊取主機敏感信息 漏洞描述 當用戶安裝受影響版本的 0xme5war/apicli 等NPM組件包時會竊取用戶的主機名、用戶名、工作目錄、IP地址等信息并發送到攻擊者的電報地址(botToken “7699295118:AAF6pb7t718vjHWHwFQlZOastZQYHL8IVDE”&#x…

計算機網絡:組播和多播有什么區別?

在網絡通信中&#xff0c;“組播”和“多播”其實是同一概念的不同中文翻譯&#xff0c;它們對應的英文都是 Multicast。二者本質上沒有技術區別&#xff0c;只是因翻譯習慣不同而產生的兩種表述&#xff0c;在實際應用中可以通用。 不過&#xff0c;為了更清晰地理解這個概念&…

Amazon Q Developer:AI 增強編碼生產力的未來

Amazon Q Developer&#xff1a;重塑編碼體驗的 AI 助手 在如今快節奏的軟件開發世界中&#xff0c;開發者們始終在尋找能平衡生產力與探索欲的工具。而 Amazon 推出的 Amazon Q Developer&#xff0c;這款可嵌入 Visual Studio Code 等主流 IDE 的 AI 編碼助手&#xff0c;無疑…

linux eval命令的使用方法介紹

在這篇文章中&#xff0c;讓我們來詳細地介紹一下 Linux 中另一個非常強大但也極其危險的命令&#xff1a;eval。 eval 是一個 shell 內置命令&#xff0c;它的名字是 “evaluate”&#xff08;評估&#xff09;的縮寫。它的作用是將緊跟其后的參數&#xff08;一個或多個字符串…

JavaWeb筆記2-JavaScriptVueAjax

1. JavaScript 1.1 基礎介紹 JavaScript(簡稱&#xff1a;JS)是一門跨平臺、面向對象的腳本語言&#xff0c;是用來控制網頁行為&#xff0c;實現頁面的交互效果。JavaScript和Java是完全不同的語言&#xff0c;但基本語法類似組成 ECMAScript: 規定了JS基礎語法核心知識&…

代碼隨想錄刷題Day23

右旋字符串 這道題是比較常規的對字符串的復制操作&#xff0c;找到右旋部分的分界點是關鍵 代碼直接貼出來&#xff1a; #include<stdio.h> #include<string.h> int main(){int k;char s[10000];scanf("%d %s",&k,s);int cnt 0;for(int i str…

機器學習sklearn:編碼、啞變量、二值化和分段

就是轉換為數值類型方便機器學習模型處理一、編碼這里舉例將Survived這一行的數據轉換為編碼&#xff0c;原本是字符串類型2、將標簽編碼并賦值回去from sklearn.preprocessing import LabelEncoder y data.iloc[:, -1] # 最后一列拿出來 print(y) le LabelEncoder() le …

嵌入式八股文總結(ARM篇)

嵌入式開發中使用的通常是ARM芯片&#xff0c;在此總結一些面試常問的問題&#xff0c;希望可以和大家一起進步。&#xff08;持續更新中……&#xff09; 目錄 1. 介紹一下I2C的傳輸時序 2. I2C為什么加上拉電阻&#xff0c;為什么使用開漏輸出 3. I2C能接多少個設備&…

TCL --- 列表_part2

0 回顧 列表part0和part1描述了列表的創建&#xff0c;修改&#xff0c;獲取&#xff0c;搜索等相關命令。接下來這篇文章將介紹列表的排序和拼接。通過這三篇文章的描述&#xff0c;詳細大家對列表具有一個詳細并且系統的認識。 1 排序 排序是一個老生常談的話題。最最最常見的…

Kafka 單機多 Broker 實例集群搭建 | 詳情

全文目錄&#xff1a;開篇語前言1. Kafka 集群架構2. 環境要求2.1 安裝 Java2.2 安裝 Zookeeper3. 安裝 Kafka4. 創建 Topic4.1 查看創建的 Topic5. 測試 Kafka 集群5.1 生產者&#xff08;Producer&#xff09;測試5.2 消費者&#xff08;Consumer&#xff09;測試6. 小結文末…

Ajax——異步前后端交互提升OA系統性能體驗

本文介紹了Ajax中的基礎使用&#xff0c;包括XMLHttpRequest的狀態變化、并使用BMI 場景的示例進行介紹&#xff0c;以及結合 DAO 和 Servlet 處理OA系統復雜業務邏輯和JSON數據的處理等等。 本文目錄一、Ajax 基礎html頁面二、 XMLHttpRequestXMLHttpRequest的狀態變化同步和異…

【最后一個單詞的長度】

思路 逆向遍歷&#xff1a; 從字符串末尾開始向前遍歷&#xff0c;跳過末尾的空格&#xff0c;直到找到非空格字符。 遇到非空格字符時開始計數&#xff0c;直到再次遇到空格或字符串開頭。 狀態標記&#xff1a; 使用 state 標記是否已經進入最后一個單詞的計數階段&#xff1…

OpenCV學習 day3

一、灰度實驗 將彩色圖像轉換為灰度圖像的過程稱為灰度化&#xff0c;這種做法在圖像處理中和計算機視覺領域非常常見 1、灰度圖 灰度圖是一種 單通道圖像&#xff0c;每個像素僅存儲 亮度信息&#xff08;0純黑&#xff0c;255純白&#xff09;&#xff0c;沒有顏色信息&#…

基于單片機一氧化碳CO檢測/煤氣防中毒檢測報警系統

傳送門 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品題目速選一覽表 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品題目功能速覽 概述 基于單片機的CO檢測系統通過傳感器實時監測環境中的一氧化碳濃度&#xff0c;結合信號處理電路與…

前端-移動Web-day3

目錄 1、視口 2、rem體驗 3、rem基本使用 4、媒體查詢 5、rem適配 6、rem布局 7、less-體驗 8、less-注釋 9、less-運算 10、less-嵌套 11、less-變量 12、less-導入 13、less-導出 14、less-禁止導出 15、案例-極速問診 1、視口 <!DOCTYPE html> <htm…