【DIY系列:自己动手写分布式搜索引擎】第28篇:多线程写索引与索引分发修复

发布时间:2026/11/4 7:14:00
【DIY系列:自己动手写分布式搜索引擎】第28篇:多线程写索引与索引分发修复

上一篇【第27篇】灵活索引与定制存储结构
下一篇【第29篇】索引文件格式详解:Lucene 磁盘上的秘密


摘要

前面咱们都是单线程 demo,真实世界里建索引基本是多线程甚至多进程狂写。Lucene 的 IndexWriter 天生线程安全,多个线程共用一个实例并发 add 是标准姿势。这回讲清楚它的并发模型、近实时读者(NRT Reader)怎么做到写入即可见、索引万一损坏了怎么用 CheckIndex 救命,以及怎么把一套索引拷到多台机器去服务。

一、IndexWriter 的并发模型

先给结论:IndexWriter 是线程安全的,你完全可以让 N 个线程拿着同一个 writer 实例往里 addDocument。Lucene 内部用一个线程安全的缓冲区 + 后台刷盘来消化并发写入,你不用自己加锁。

但要注意,一个索引目录同一时刻只能有一个 IndexWriter 打开(靠 write.lock 文件保证)。这是为了防止两个进程各自写,把段文件写花。所以"多进程写同一目录"这条路被堵死了,多进程场景要么每人写自己的目录再合并,要么走网络层的协调(后面 SolrCloud/ES 就是这么干的)。

【多线程写同一个 IndexWriter】 线程A ─┐ 线程B ─┼─▶ 同一个 IndexWriter ─▶ 内部缓冲 ─▶ 刷盘成段 线程C ─┘ (线程安全) (串行化落盘) ❌ 两个进程各开一个 IndexWriter 写同目录 → write.lock 冲突

我刚用时犯过傻:为了"提速"给每台机器都开 writer 写共享 NFS 上的同一目录,结果直接 lock 报错。多线程用一个 writer 是对的,多进程写同目录是错的,这个边界一定划清。

二、近实时搜索:写入即可见

传统认知里,“建了索引要重新打开 reader 才能看到新数据”。Lucene 的近实时(NRT)把这个延迟压到秒级:你写完提交,调用DirectoryReader.openIfChanged(oldReader)拿到一个新 reader,新段立刻可见,不用重建整个索引。

DirectoryReaderreader=DirectoryReader.open(dir);// ... 写入并提交 ...DirectoryReadernewReader=DirectoryReader.openIfChanged(reader);if(newReader!=null){reader.close();// 关掉旧的reader=newReader;// 换成能看到新数据的IndexSearchersearcher=newIndexSearcher(reader);}

这里的精髓是 openIfChanged 只加载"变化的段",没变的段复用,所以重开极快。这就是搜索框里"刚发的帖子马上能搜到"的底层支撑。对比咱们手写玩具:每次都得把整个 HashMap 重读一遍,数据一大就卡,这就是工业级和玩具的差距。

【NRT 可见性时间线】 t0 写入缓冲 t1 flush 成新段(磁盘上) t2 commit 登记 t3 openIfChanged → 新 reader 看到新段 可见延迟 ≈ 从 t0 到 t3,通常秒级

需要提醒:NRT 的"实时"是相对的,默认 commit/refresh 有间隔。你想更实时就调短间隔,但代价是段更碎、IO 更频繁,又是那套权衡。后面讲 ES 的 refresh 还会碰到它。

三、索引损坏了怎么办:CheckIndex

再稳的系统也架不住断电、磁盘坏道、误删文件。Lucene 索引一旦某个段文件残缺,打开时可能直接抛异常。这时候别慌,它自带一个"体检工具" CheckIndex。

// 命令行用法javaorg.apache.lucene.index.CheckIndex/data/idx// 代码里也能调CheckIndexci=newCheckIndex(dir);CheckIndex.Statusstatus=ci.checkIndex();if(!status.clean){// 有损坏,可以尝试修复(谨慎!会丢弃坏段里的文档)ci.exorciseIndex();// 把检查不通过的段剔除}

CheckIndex 会逐个段校验:词典和倒排对不对得上、文档计数准不准、各个文件 checksum 是否一致。它能干两件事:

  • 诊断:告诉你哪个段坏在哪,不动数据,先看清病情。
  • 修复(exorcise):把检查不过的段直接剔除。注意这是"丢卒保车",坏段里的文档会丢,但至少救回其余健康的段,让索引重新能打开。
【索引损坏应急流程】 发现打不开 / 报错 │ ▼ CheckIndex.checkIndex() ── 诊断,不破坏 │ ├─ clean=true → 其实没坏,查别的原因 │ └─ clean=false → 备份后 exorciseIndex() 剔除坏段 (坏段文档丢失,健康段保住)

我建议任何生产索引都定期跑 CheckIndex 做体检,尤其在大版本升级、迁移磁盘之后。它就像给索引做胃镜,花不了多少时间,能提前发现隐患。

四、把索引分发到多台机器

索引建在一次,服务要扛高并发,常见套路是:在一台机器上把索引建好,然后原样拷贝分发到 N 台搜索节点,每台本地开 FSDirectory 服务。因为 Lucene 的索引就是一串普通的段文件,拷贝即可用,不需要什么特殊协议。

【索引分发雏形】 建索引机 ──生成段文件──▶ 拷贝/rsync ──▶ 节点A(/data/idx) ──▶ 节点B(/data/idx) ──▶ 节点C(/data/idx) 每台节点: DirectoryReader.open(本地 idx) → 独立服务

这种"建好再分发"的模式简单可靠,适合只读型索引(比如词典、商品目录)。它的问题是更新麻烦:改了数据得全量重发或增量补段。更动态的"边写边多机协同"就超出了裸 Lucene 的能力,要靠 SolrCloud、ES 的分片和副本机制,那是以后的事。

但今天这篇你记住两点就够:第一,Lucene 自身的并发模型是"单 writer 多线程,禁止多进程写同目录";第二,索引文件就是普通文件,可拷贝、可分发、可体检修复。这两点正是裸 Lucene 和分布式搜索之间的桥。

本篇小结

IndexWriter 线程安全,多线程共用一个实例是标准写法,但一个目录同一时刻只能有一个 writer(write.lock 兜底),多进程写同目录行不通。近实时靠 openIfChanged 秒级看到新段,不用重建索引。索引坏了用 CheckIndex 体检,必要时 exorcise 丢卒保车。索引文件是普通段文件,可直接拷贝分发到多机。下一篇我们把镜头拉到磁盘上,逐文件拆开 Lucene 的索引格式,看看这些段到底长什么样。

上一篇【第27篇】灵活索引与定制存储结构
下一篇【第29篇】索引文件格式详解:Lucene 磁盘上的秘密