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)"概念非常相似------是一种增量同步机制。