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);

内部步骤:

  1. DWPT-A 处理 doc
  2. 调用 deleteQueue.add(delTermNode, dwptSlice)
    • 全局队列追加 delTermNode
    • dwptSlice.sliceTail = delTermNode ← 关键!
  3. 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 的核心价值

  1. 全局顺序保证:所有 delete 严格按调用顺序执行
  2. 跨 DWPT 一致性:任何 delete 对所有 segment 可见
  3. 高效并发:无锁读(slice 是线程局部),仅写入加锁
  4. 内存安全:GC 自动回收已处理节点
  5. 支持 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 所指的位置


❓ 五、为什么这样设计?

  1. 避免重复处理

    如果不记录 head,每次 flush 都从头开始遍历 delete 队列 → 性能灾难

  2. 保证恰好一次语义

    每个 delete 节点被每个 DWPT 恰好处理一次

  3. 支持并发

    每个 DWPT 独立维护自己的进度,互不影响

  4. GC 友好

    一旦所有 DWPT 的 sliceHead 都越过了某个节点,该节点就不可达 → 被回收


✅ 六、总结回答

"上次提交"指的是:当前 DWPT 上一次 flush 时,通过 DeleteSlice.reset() 记录的删除处理截止位置(即当时的 sliceTail)。

它是 DWPT 私有的逻辑进度标记,用于确保:

  • 不遗漏新 delete
  • 不重复处理旧 delete
  • 正确实现 update/delete 的语义顺序

这和数据库中的"LSN(Log Sequence Number)"或"游标(cursor)"概念非常相似------是一种增量同步机制

相关推荐
marlondu11 小时前
ScopedValue:Java 21 引入的结构化作用域值
java
日月云棠11 小时前
12 Dubbo 2.7 服务发布全流程源码解析
java·后端
沈阳信息学奥赛培训11 小时前
C++ 位运算练习题
开发语言·c++
Oj92q85H511 小时前
如何在Dev-C++中使用TDM-GCC编译多个文件
开发语言·c++
wengqidaifeng11 小时前
C++从菜鸟到强手:2.类和对象(下)—— 进阶特性与完整日期类实现
开发语言·c++
专注VB编程开发20年11 小时前
JAVA动态调用函数,数字类型,Java 反射允许自动拓宽类型。
开发语言·python
用户2986985301411 小时前
告别手动复制:Java 拆分 Word 文档的两种实用方案
java·后端
ujainu小11 小时前
CANN hixl:大模型 PD 分离场景的零拷贝通信库
android·java·缓存
z2005093011 小时前
今日算法(组合问题III)(回溯的使用)
java·算法·leetcode