这篇文章聚焦 TinyDb 的“写入主路径”,即 InsertDocument / UpdateDocument / DeleteDocument。
1. 插入路径总览
入口(同步)在 TinyDbEngine.InsertDocument(string col, BsonDocument doc):
var st = GetCollectionState(col);
var idx = GetIndexManager(col);
var pr = PrepareDocumentForInsert(col, doc, out var id);
_metadataManager.ValidateDocumentForWrite(col, pr, _options.SchemaValidationMode);
lock (st.WriteSyncRoot)
{
res = InsertPreparedDocument(col, pr, id, st, idx, true);
}
EnsureWriteDurability();关键动作:
准备文档(补
_id、强制_collection)。Schema 校验(可配置
None/Required/Strict)。写锁内写入页与内存索引。
锁外执行持久化保障(由
WriteConcern决定)。
2. PrepareDocumentForInsert 的两个优化点
2.1 快速路径
如果文档已带 _id 且 _collection 正确,直接返回原文档,避免重建。
2.2 不可变文档重建
BsonDocument 是不可变结构,必要时会用 ImmutableDictionary.Builder 批量构建新文档:
缺
_id:自动生成ObjectId。_collection不匹配:覆盖为当前集合名。
3. 大文档分离机制
插入时会序列化文档并检测大小:
if (LargeDocumentStorage.RequiresLargeDocumentStorage(size, maxSinglePageSize))
{
var largePageId = _largeDocumentStorage.StoreLargeDocument(bytes, col);
doc = CreateLargeDocumentIndexDocument(id, col, largePageId, size);
}即:
原大文档被拆分写入
LargeDocumentData页链。数据页中只保留一个“索引文档”(包含
_largeDocumentIndex、_isLargeDocument等元字段)。
读取时 ResolveLargeDocument 再回读真实文档。
4. 更新路径中的“原地改写 vs 搬迁重插”
UpdateDocument 的核心分支:
按
_id定位页与条目。用新 BSON 替换条目。
判断页面是否还能容纳:
能容纳:原页重写。
不能容纳:回滚当前替换,先从原页删除,再走插入路径到新页。
这解释了为什么“更新变大”的文档会触发位置变化。
5. 删除路径
DeleteDocument 逻辑:
_id定位 -> 移除条目。更新内存位置索引(
MemoryDocumentIndex)。若页变空:
从集合拥有页集合移除。
交给
PageManager.FreePage回收。更新 Header 的
UsedPages。
删除关联索引项。
若是大文档索引文档,额外删除其大文档页链。
6. 批量插入路径为什么快
InsertDocuments 相比单条插入,多了几个性能关键点:
使用
PooledBufferWriter降低分配。复用当前可写页,减少页切换。
页面持久化集中执行。
索引更新集中执行。
同时它收集异常,最终可能抛 AggregateException,便于一次性观察批处理中多处失败。
7. 持久化语义:WriteConcern
写入最终一致性由 FlushScheduler.EnsureDurability 负责:
None:仅内存与后台刷盘。Journaled:WAL 先刷盘,数据页异步落盘。Synced:WAL 与脏页都在返回前同步落盘。
8. 对开发者的实践建议
大批量写入优先走批量 API,避免逐条调用。
频繁更新且体积变化大的文档,尽量控制字段膨胀,减少搬迁。
关键业务(资金、订单状态)使用
WriteConcern.Synced。高吞吐日志场景可考虑
Journaled/None+ 周期性 checkpoint。
9. 小结
TinyDb 写入路径的核心机制是:
不可变文档 + 页重写。
大文档外置存储。
内存位置索引与磁盘页状态同步维护。
通过
WriteConcern在性能与安全之间做明确选择。
下一篇继续拆查询链路:表达式如何转执行计划,如何命中索引。
发表评论