超越边界: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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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…