DocumentsWriterDeleteQueue

DocumentsWriterDeleteQueue Lucene 核心索引模块中的DocumentsWriterDeleteQueue类它是 Lucene 实现高并发、强一致性删除delete和更新update语义的关键基础设施。我们可以用一句话概括它的作用✅DocumentsWriterDeleteQueue是一个全局的、非阻塞的、单向链表结构的“待处理删除操作队列”用于在多线程写入add/update/delete场景下保证所有删除操作按提交顺序被正确应用到每个即将 flush 的 segment 上。 一、为什么需要它——问题背景在 Lucene 中多个线程可以同时调用addDocument()、updateDocument()、deleteDocuments()每个线程有自己的DocumentsWriterPerThreadDWPT独立缓冲文档但删除操作是全局的一个delete(term)应该影响所有 segment包括未来 flush 出来的挑战如何让每个 DWPT 在 flush 时知道“截至此刻哪些 delete 已经发生”并且要保证先 add 后 delete → 文档不出现先 delete 后 add → 文档出现这就需要一个全局有序的 delete 日志而DocumentsWriterDeleteQueue就是这个日志。 二、核心设计链表 DeleteSlice1.全局链表结构private volatile Node? tail;所有 delete 操作term/query/doc-values update都被封装为Node追加到链表尾部链表是单向、无头只有 tail、带哨兵节点sentinel追加操作是synchronized的保证严格顺序2.每个消费者持有一个 DeleteSliceclass DeleteSlice { Node? sliceHead; // 上次处理到的位置不包含 Node? sliceTail; // 本次需要处理到的位置包含 }每个 DWPT有自己的DeleteSlice全局 delete pool也有一个globalSliceslice表示“我需要处理从 head 到 tail 之间的 delete” 这种设计让 GC 自动回收已处理的节点只要没有 slice 引用它⚙️ 三、关键方法解析方法作用add(Node, DeleteSlice)用于updateDocument(doc, delTerm)1. 将 delTerm 加入全局队列2.原子性地更新调用者的 slice.tail 新节点→ 保证该 delete 会被本 DWPT 在 flush 时应用freezeGlobalBuffer(DeleteSlice callerSlice)DWPT flush 前调用1. 锁住全局 buffer2. 将 callerSlice.tail 推进到当前 tail3. 返回一个FrozenBufferedUpdates快照包含所有 delete→ 用于构建FlushedSegmenttryApplyGlobalSlice()异步尝试将新 delete 应用到globalBufferedUpdates供后续 merge 或 searcher 使用newSlice()为新 DWPT 创建初始 slice指向当前 tail 四、典型流程updateDocument(doc, delTerm)这是最能体现其价值的场景indexWriter.updateDocument(new Term(id, 123), doc);内部步骤DWPT-A 处理 doc调用deleteQueue.add(delTermNode, dwptSlice)全局队列追加delTermNodedwptSlice.sliceTail delTermNode← 关键DWPT-A flush 时调用freezeGlobalBuffer(dwptSlice)将 slice.tail 推进到最新 tail可能包含其他 deletedwptSlice.apply(...)→ 应用所有 delete包括自己的 delTerm如果 doc 匹配delTerm→ 不写入 segment✅ 即使多个线程同时 update 同一个 term也能保证只有一个成功写入因为 slice.tail 原子更新 五、全局 vs 局部视角视角说明全局队列所有 delete 的“事实来源”严格有序DWPT 的 slice“我关心哪些 delete” —— 从上次 flush 到现在globalSlice“哪些 delete 还没应用到已提交的 segments”→ 用于生成.del文件或 merge 时应用 六、类比理解想象一个多人协作编辑的 Google Docs每个人DWPT在自己的草稿区写内容所有“删除某段文字”的操作delete被记录在一个共享操作日志DeleteQueue中当某人要“提交草稿”flush时他查看日志“从我上次提交后有哪些删除”把这些删除应用到自己的草稿上提交最终版本✅DeleteSlice就是每个人的“日志阅读进度条”✅ 七、总结DocumentsWriterDeleteQueue的核心价值全局顺序保证所有 delete 严格按调用顺序执行跨 DWPT 一致性任何 delete 对所有 segment 可见高效并发无锁读slice 是线程局部仅写入加锁内存安全GC 自动回收已处理节点支持 update 语义通过add(node, slice)实现原子性它是 Lucene 实现“实时更新”先删后加和“高吞吐写入”的基石之一。这个“上次提交”在 Lucene 的上下文中并不是指 segment 已经写入磁盘并对外可见而是指✅“当前 DWPT 上一次完成 flush 准备即调用freezeGlobalBuffer并重置 DeleteSlice的那个时间点”换句话说“上次提交” “上次 flush 时我处理 delete 的截止位置”。 一、为什么不是“真正提交到索引”因为DWPT 的 flush 是异步的flush 完成后segment 还要经过 publish 才对 searcher 可见DeleteSlice 是 DWPT 私有的状态只关心“我自己缓冲的文档需要应用哪些 delete”所以这里的“提交”是DWPT 内部视角的“逻辑提交点”不是全局索引的提交。 二、具体解释“上次提交”到底是什么每个 DWPT 持有一个DeleteSlice它有两个指针Node? sliceHead; // ← “上次提交”的位置即上次 flush 时处理到的 tail Node? sliceTail; // ← 当前需要处理到的位置生命周期示例时间操作全局队列DWPT-A 的 DeleteSliceT0DWPT-A 创建[sentinel]head tail sentinelT1Thread-1:delete(term1)[sentinel → term1]不变T2Thread-2:add(doc1)→ DWPT-A同上不变T3DWPT-A flush同上调用freezeGlobalBuffer()-sliceTail推进到term1- 应用term1-reset()→head tail term1T4Thread-3:delete(term2)[... → term2]不变T5Thread-4:add(doc2)→ DWPT-A同上不变T6DWPT-A 再次 flush同上freezeGlobalBuffer()- 发现sliceTail (term1) ! globalTail (term2)- 推进 tail 到term2- 应用term2✅ 所以“上次提交” 上一次 flush 时reset()后的sliceHead等于当时的sliceTail⚙️ 三、代码中的体现在DeleteSlice.reset()中void reset() { sliceHead sliceTail; // ← 关键将 head 移到当前 tail }而reset()被调用的地方是// 在 freezeGlobalBuffer - apply 之后 deleteSlice.apply(...); deleteSlice.reset(); // ← 标记“本次 flush 已处理到此处”所以sliceHead始终指向“上次 flush 时处理完的最后一个 delete 节点”下次 flush 时从head.next开始处理直到新的tail️ 四、图解“上次提交”的含义全局删除队列: [sentinel] → [del1] → [del2] → [del3] → [del4] ↑ ↑ sliceHead (上次提交) sliceTail (本次要处理到)DWPT 在 T3 flush 时处理了 del1、del2 → reset 后head del2现在 T6 flush发现新来了 del3、del4 → 处理它们处理完后 reset →head del4✅ “上次提交” sliceHead所指的位置❓ 五、为什么这样设计避免重复处理如果不记录head每次 flush 都从头开始遍历 delete 队列 → 性能灾难保证恰好一次语义每个 delete 节点被每个 DWPT恰好处理一次支持并发每个 DWPT 独立维护自己的进度互不影响GC 友好一旦所有 DWPT 的sliceHead都越过了某个节点该节点就不可达 → 被回收✅ 六、总结回答“上次提交”指的是当前 DWPT 上一次 flush 时通过DeleteSlice.reset()记录的删除处理截止位置即当时的sliceTail。它是DWPT 私有的逻辑进度标记用于确保不遗漏新 delete不重复处理旧 delete正确实现 update/delete 的语义顺序这和数据库中的“LSNLog Sequence Number”或“游标cursor”概念非常相似——是一种增量同步机制。