这篇文章聚焦 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();

关键动作:

  1. 准备文档(补 _id、强制 _collection)。

  2. Schema 校验(可配置 None/Required/Strict)。

  3. 写锁内写入页与内存索引。

  4. 锁外执行持久化保障(由 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 的核心分支:

  1. _id 定位页与条目。

  2. 用新 BSON 替换条目。

  3. 判断页面是否还能容纳:

  • 能容纳:原页重写。

  • 不能容纳:回滚当前替换,先从原页删除,再走插入路径到新页。

这解释了为什么“更新变大”的文档会触发位置变化。

5. 删除路径

DeleteDocument 逻辑:

  1. _id 定位 -> 移除条目。

  2. 更新内存位置索引(MemoryDocumentIndex)。

  3. 若页变空:

  • 从集合拥有页集合移除。

  • 交给 PageManager.FreePage 回收。

  • 更新 Header 的 UsedPages

  1. 删除关联索引项。

  2. 若是大文档索引文档,额外删除其大文档页链。

6. 批量插入路径为什么快

InsertDocuments 相比单条插入,多了几个性能关键点:

  1. 使用 PooledBufferWriter 降低分配。

  2. 复用当前可写页,减少页切换。

  3. 页面持久化集中执行。

  4. 索引更新集中执行。

同时它收集异常,最终可能抛 AggregateException,便于一次性观察批处理中多处失败。

7. 持久化语义:WriteConcern

写入最终一致性由 FlushScheduler.EnsureDurability 负责:

  • None:仅内存与后台刷盘。

  • Journaled:WAL 先刷盘,数据页异步落盘。

  • Synced:WAL 与脏页都在返回前同步落盘。

8. 对开发者的实践建议

  1. 大批量写入优先走批量 API,避免逐条调用。

  2. 频繁更新且体积变化大的文档,尽量控制字段膨胀,减少搬迁。

  3. 关键业务(资金、订单状态)使用 WriteConcern.Synced

  4. 高吞吐日志场景可考虑 Journaled/None + 周期性 checkpoint。

9. 小结

TinyDb 写入路径的核心机制是:

  • 不可变文档 + 页重写。

  • 大文档外置存储。

  • 内存位置索引与磁盘页状态同步维护。

  • 通过 WriteConcern 在性能与安全之间做明确选择。

下一篇继续拆查询链路:表达式如何转执行计划,如何命中索引。