DocumentsWriterFlushQueue(或更准确地说,其背后的 FlushTicket 机制)的核心作用,确实是控制 不同 flush 事件(即不同 DWPT 的 flush)之间的全局 publish 顺序,而不是控制单个 flush 内部的操作顺序。
下面我们深入剖析:
🔑 核心概念澄清
首先,Lucene 源码中并没有一个直接叫 DocumentsWriterFlushQueue 的 public 类。你提到的这个名称,实际上是指 DocumentsWriterFlushControl 内部管理的一个隐式的、基于 ticket 的队列机制。
真正负责顺序控制的是 FlushTicket 和 publishFlushedSegments 这一过程。
✅ FlushTicket 机制:跨 flush 单元的顺序控制器
📌 它解决的问题:
当多个线程(或同一个线程在不同时间)触发了多个 DWPT 的 flush,这些 flush 产生的 segment 和 deletes 必须按照"逻辑发生顺序"被 publish 到全局的
SegmentInfos中。
🧠 工作流程:
-
申请 Ticket :
-
每当一个 DWPT 完成 flush(无论是否生成 segment),它会向
DocumentsWriterFlushControl申请一个唯一的、单调递增的flushTicket。long ticket = flushControl.markTicket();
-
-
排队等待 Publish :
- 所有完成 flush 的单元都带着自己的
ticket进入一个逻辑上的等待队列。
- 所有完成 flush 的单元都带着自己的
-
按 Ticket 顺序 Publish :
- Lucene 有一个专门的 publish 线程(通常是调用
commit()或getReader()的线程)会:- 获取当前最小的未 publish 的 ticket。
- 严格按照 ticket 从小到大的顺序 ,将对应的 segment 和/或 deletes 应用到全局的
SegmentInfos。
- 这个过程确保了:ticket 小的 flush 结果一定先于 ticket 大的结果对搜索可见。
- Lucene 有一个专门的 publish 线程(通常是调用
🆚 对比:两种不同的"顺序"
| 顺序类型 | 范围 | 控制者 | 机制 |
|---|---|---|---|
| DWPT 内部顺序 | 同一个 flush 单元内的 add/delete | DocumentsWriterPerThread |
内存重放 + in-memory deduplication |
| DWPT 间全局顺序 | 不同 flush 单元之间的先后 | DocumentsWriterFlushControl + FlushTicket |
Ticket 队列 + 顺序 publish |
- "一批操作中间的 delete" → 属于第一种,与 ticket 无关。
- "flush 之间的顺序" → 正是第二种,由 ticket 机制保证。
🧪 举个例子说明 FlushTicket 的必要性
假设两个线程并发操作:
- Thread A :
add(doc1)→ DWPT-A flush → ticket=10 - Thread B :
delete(term)→ DWPT-B flush → ticket=11
如果没有 ticket 机制,可能出现:
- DWPT-B 的 flush 先完成,先 publish deletes。
- 此时全局
SegmentInfos还没有 doc1 所在的 segment。 - 结果:delete 无法生效,doc1 永远搜不到!
有了 ticket 机制:
- Publish 线程发现 ticket=10 < ticket=11。
- 先 publish ticket=10 的 segment(包含 doc1)。
- 再 publish ticket=11 的 deletes。
- 结果:delete 成功删除 doc1,语义正确。
📌 总结回答你的问题
"DocumentsWriterFlushQueue 也就是说这个是控制 flush 之间的顺序的"
✅ 完全正确! 更精确地说:
DocumentsWriterFlushControl通过分配FlushTicket,建立了一个逻辑上的 flush 事件队列。- 这个队列保证了所有 flush 产生的结果(segment 或 deletes)能够按照严格的、全局一致的顺序被 publish。
- 这是 Lucene 实现"近实时搜索"(NRT)和"多线程写入一致性"的基石。
DocumentsWriterFlushQueue 的核心作用,就是控制多个 flush 操作(无论是来自 DWPT 的 segment flush,还是全局删除 flush)之间的 publish 顺序 ,而这个顺序正是通过 ticket 机制来实现的。
下面我们结合源码,逐层解析它的设计意图和工作机制。
✅ 核心职责:保证 flush 结果的全局 publish 顺序
在高并发写入时,多个线程可能同时触发 flush:
- Thread A: flush DWPT → 生成 segment
_0 - Thread B: 触发全局 delete flush → 生成
FrozenBufferedUpdates - Thread C: flush 另一个 DWPT → 生成 segment
_1
这些 flush 在物理上是并行执行的 (比如异步写磁盘),但它们对索引的"可见性"必须按逻辑顺序生效。
📌
DocumentsWriterFlushQueue就是用来确保:先申请 ticket 的 flush,先 publish 到SegmentInfos中。
🔍 源码关键点解析
1. ticketCount + queue:FIFO 队列
private final Queue<FlushTicket> queue = new LinkedList<>();
private final AtomicInteger ticketCount = new AtomicInteger();
- 每次调用
addFlushTicket()或addDeletes(),都会:incTickets()→ 原子递增计数器- 创建
FlushTicket并加入队列尾部
- 队列顺序 = ticket 申请顺序 = 全局 publish 顺序
💡 这是一个典型的 生产者-消费者模型:flush 是生产者,publish 是消费者。
2. 两种 ticket 类型
从构造函数可以看出:
new FlushTicket(frozenBufferedUpdates, false); // 删除 flush(无 segment)
new FlushTicket(dwpt.prepareFlush(), true); // DWPT flush(有 segment)
hasSegment = true:来自DWPT.flush(),最终会关联一个FlushedSegmenthasSegment = false:来自全局删除队列 freeze,只包含FrozenBufferedUpdates
✅ 两者平等排队,统一由 ticket 顺序决定谁先 publish。
3. 异步 flush + 同步 publish 解耦
注意这个设计精妙之处:
synchronized FlushTicket addFlushTicket(DocumentsWriterPerThread dwpt) {
incTickets();
FlushTicket ticket = new FlushTicket(dwpt.prepareFlush(), true);
queue.add(ticket);
return ticket; // 注意:此时 segment 还没写完!
}
-
dwpt.prepareFlush()只是准备数据(如冻结删除、分配 docID 等) -
真正的磁盘 I/O(写 segment 文件)是在后台异步执行的
-
写完后,通过
addSegment(ticket, segment)回填结果:synchronized void addSegment(FlushTicket ticket, FlushedSegment segment) {
ticket.setSegment(segment); // 唤醒 publish 条件
}
4. Publish 条件:canPublish()
boolean canPublish() {
return hasSegment == false || segment != null || failed;
}
- 对于 delete ticket (
hasSegment=false):立即可 publish - 对于 segment ticket (
hasSegment=true):必须等segment != null(即异步 flush 完成)才能 publish
✅ 这就实现了:即使 segment flush 慢,也不会阻塞队列;但 publish 一定按 ticket 顺序进行。
5. Publish 过程:innerPurge()
while (true) {
head = queue.peek();
if (head != null && head.canPublish()) {
consumer.accept(head); // 实际 publish(更新 SegmentInfos)
queue.poll(); // 出队
decTickets();
} else break;
}
- 严格 FIFO :只有队首 ticket 满足
canPublish()才能 publish - 如果队首是 segment ticket 但还没 flush 完(
segment == null),整个队列都会被阻塞(stall)
⚠️ 这就是为什么一个慢 flush 会拖慢全局可见性的原因!
🧠 与你之前问题的联系
| 你的疑问 | 此类的解答 |
|---|---|
| "flush 顺序如何保证?" | → DocumentsWriterFlushQueue 用 FIFO ticket 队列保证 |
| "delete 和 segment 谁先 publish?" | → 谁先申请 ticket 谁先 publish,与操作类型无关 |
| "ticket 有什么用?" | → ticket 是 publish 队列的排队凭证,确保跨 DWPT 顺序 |
✅ 总结
DocumentsWriterFlushQueue是 Lucene 并发写入模型中的"顺序仲裁器":
- 它将 异步 flush (物理 I/O)和 同步 publish(逻辑可见性)解耦
- 通过 FIFO ticket 队列,确保所有 flush 结果(segment 或 deletes)按申请顺序 publish
- 从而保证:delete 不会早于它依赖的 segment 生效
DocumentsWriterFlushQueue ,正是 Lucene 实现 "高并发写入 + 强一致性语义" 的关键基础设施之一。👏
简言之:是的,它就是用来控制 flush 之间 publish 顺序的。