DocumentsWriterDeleteQueue
Lucene 核心索引模块中的DocumentsWriterDeleteQueue类,它是 Lucene 实现高并发、强一致性删除(delete)和更新(update)语义的关键基础设施。
我们可以用一句话概括它的作用:
✅
DocumentsWriterDeleteQueue是一个全局的、非阻塞的、单向链表结构的“待处理删除操作队列”,用于在多线程写入(add/update/delete)场景下,保证所有删除操作按提交顺序被正确应用到每个即将 flush 的 segment 上。
🔍 一、为什么需要它?——问题背景
在 Lucene 中:
- 多个线程可以同时调用
addDocument()、updateDocument()、deleteDocuments() - 每个线程有自己的
DocumentsWriterPerThread(DWPT),独立缓冲文档 - 但删除操作是全局的:一个
delete(term)应该影响所有 segment(包括未来 flush 出来的)
挑战:
如何让每个 DWPT 在 flush 时,知道“截至此刻,哪些 delete 已经发生”?
并且要保证:先 add 后 delete → 文档不出现;先 delete 后 add → 文档出现
这就需要一个全局有序的 delete 日志,而DocumentsWriterDeleteQueue就是这个日志。
🧱 二、核心设计:链表 + DeleteSlice
1.全局链表结构
private volatile Node<?> tail;- 所有 delete 操作(term/query/doc-values update)都被封装为
Node,追加到链表尾部 - 链表是单向、无头(只有 tail)、带哨兵节点(sentinel)
- 追加操作是
synchronized的,保证严格顺序
2.每个消费者持有一个 DeleteSlice
class DeleteSlice { Node<?> sliceHead; // 上次处理到的位置(不包含) Node<?> sliceTail; // 本次需要处理到的位置(包含) }- 每个 DWPT有自己的
DeleteSlice - 全局 delete pool也有一个
globalSlice slice表示“我需要处理从 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. 锁住全局 buffer 2. 将 callerSlice.tail 推进到当前 tail 3. 返回一个 FrozenBufferedUpdates快照(包含所有 delete)→ 用于构建 FlushedSegment |
tryApplyGlobalSlice() | 异步尝试将新 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)- 全局队列追加
delTermNode - dwptSlice.sliceTail = delTermNode← 关键!
- 全局队列追加
- DWPT-A flush 时:
- 调用
freezeGlobalBuffer(dwptSlice)- 将 slice.tail 推进到最新 tail(可能包含其他 delete)
dwptSlice.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 的 DeleteSlice |
|---|---|---|---|
| T0 | DWPT-A 创建 | [sentinel] | head = tail = sentinel |
| T1 | Thread-1:delete(term1) | [sentinel → term1] | 不变 |
| T2 | Thread-2:add(doc1)→ DWPT-A | 同上 | 不变 |
| T3 | DWPT-A flush | 同上 | 调用freezeGlobalBuffer():- sliceTail推进到term1- 应用 term1- reset()→head = tail = term1 |
| T4 | Thread-3:delete(term2) | [... → term2] | 不变 |
| T5 | Thread-4:add(doc2)→ DWPT-A | 同上 | 不变 |
| T6 | DWPT-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 的语义顺序
这和数据库中的“LSN(Log Sequence Number)”或“游标(cursor)”概念非常相似——是一种增量同步机制。
