高吞吐場景下,單線程同步調用 Solr 索引會嚴重拖慢業務響應。本文解析一個生產級 SolrIndexer 實現:基於 BlockingQueue 的生產消費模型、後台單線程批量處理、區分更新/刪除操作、超時關閉機制、異常隔離,支援每秒萬級文檔索引。
┌─────────────┐ addIndex/delIndex ┌──────────────────┐
│ 業務線程 │ ─────────────────────────▶ │ BlockingQueue │ (生產者)
└─────────────┘ Operation 對象 │ ArrayBlockingQueue
└────────┬─────────┘
│ poll()
┌────────▼─────────┐
│ 後台更新線程 │ (消費者)
│ UpdateTask.run()│
│ 批量聚合 1秒 │
│ 批量提交 Solr │
└────────┬─────────┘
│
┌────────▼─────────┐
│ Solr Server │
│ (SolrCloud/單機) │
└──────────────────┘
package com.example.solr;
import org.apache.solr.client.solrj.*;
import org.apache.solr.client.solrj.impl.*;
import org.apache.solr.client.solrj.request.*;
import org.apache.solr.common.*;
import org.slf4j.*;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
public class SolrIndexer implements Indexer, Searcher, DisposableBean {
private static final Logger log = LoggerFactory.getLogger(SolrIndexer.class);
// 配置常量
private static final long SHUTDOWN_TIMEOUT = 5 * 60 * 1000L; // 5分鐘
private static final int QUEUE_CAPACITY = 16384; // 隊列容量
private static final long BATCH_INTERVAL_MS = 1000; // 批量間隔 1秒
// 核心組件
private final SolrClient solrClient;
private final BlockingQueue inputQueue;
private final Thread updateThread;
private final AtomicBoolean running = new AtomicBoolean(true);
private final AtomicBoolean shuttingDown = new AtomicBoolean(false);
public SolrIndexer(String solrUrl) {
this.solrClient = new HttpSolrClient.Builder(solrUrl).build();
this.inputQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY);
// 啟動後台消費線程
this.updateThread = new Thread(this::consumeLoop, "SolrIndexer-Worker");
this.updateThread.start();
}
// ========== 生產者接口 ==========
@Override
public void addIndex(Indexable indexable) throws IndexingException {
checkShutdown();
if (!inputQueue.offer(new Operation(indexable, OpType.UPDATE))) {
throw new IndexingException("索引隊列已滿,拒絕新任務");
}
}
@Override
public void delIndex(Indexable indexable) throws IndexingException {
checkShutdown();
if (!inputQueue.offer(new Operation(indexable, OpType.DELETE))) {
throw new IndexingException("索引隊列已滿,拒絕新任務");
}
}
// ========== 消費者主循環 ==========
private void consumeLoop() {
while (running.get()) {
try {
syncIndices();
} catch (Throwable e) {
if (shuttingDown.get()) {
log.warn("關閉過程中異常", e);
} else {
log.error("索引處理異常", e);
}
}
}
log.info("SolrIndexer 後台線程已停止");
}
private void syncIndices() throws InterruptedException {
// 1. 取首個操作(阻塞 1 秒)
Operation first = inputQueue.poll(BATCH_INTERVAL_MS, TimeUnit.MILLISECONDS);
if (first == null) return; // 超時無數據
if (first.type == OpType.SHUTDOWN) {
first.latch.countDown();
return;
}
// 2. 批量收集:排空隊列
List batch = new ArrayList<>(inputQueue.size() + 1);
batch.add(first);
inputQueue.drainTo(batch);
// 3. 分類聚合:按類型 + 集合名 分組
Map> updateMap = new HashMap<>();
Map> deleteMap = new HashMap<>();
for (Operation op : batch) {
if (op.type == OpType.SHUTDOWN) {
op.latch.countDown();
continue;
}
String collection = op.indexable.getCollection();
if (op.type == OpType.UPDATE) {
updateMap.computeIfAbsent(collection, k -> new ArrayList<>()).add(op.indexable);
} else {
deleteMap.computeIfAbsent(collection, k -> new ArrayList<>()).add(op.indexable);
}
}
// 4. 批量提交刪除
for (Map.Entry> e : deleteMap.entrySet()) {
deleteBatch(e.getKey(), e.getValue());
}
// 5. 批量提交更新
for (Map.Entry> e : updateMap.entrySet()) {
updateBatch(e.getKey(), e.getValue());
}
}
// ========== 批量提交邏輯 ==========
private void updateBatch(String collection, List docs) {
if (docs.isEmpty()) return;
log.debug("批量更新 {} 文檔到集合 [{}]", docs.size(), collection);
UpdateRequest req = new UpdateRequest("/" + collection + "/update");
req.setAction(UpdateRequest.ACTION.COMMIT, false, false); // 軟提交
for (Indexable idx : docs) {
SolrInputDocument doc = new SolrInputDocument();
idx.getDoc().forEach((k, v) -> doc.addField(k, v));
req.add(doc);
}
processRequest(req);
}
private void deleteBatch(String collection, List docs) {
if (docs.isEmpty()) return;
log.debug("批量刪除 {} 文檔從集合 [{}]", docs.size(), collection);
UpdateRequest req = new UpdateRequest("/" + collection + "/update");
req.setAction(UpdateRequest.ACTION.COMMIT, false, false);
for (Indexable idx : docs) {
req.deleteById(idx.getId());
}
processRequest(req);
}
private void processRequest(UpdateRequest req) {
try {
req.process(solrClient);
} catch (SolrServerException | IOException e) {
log.error("Solr 請求失敗", e);
throw new IndexingException(e);
}
}
// ========== 查詢接口 ==========
@Override
public QueryResult search(Query query) throws IndexingException {
SolrQuery sq = new SolrQuery(query.getQueryString());
if (query.getFilter() != null) sq.addFilterQuery(query.getFilter());
if (query.getSort() != null) sq.addSort(query.getSortField(), query.getOrder());
sq.setStart(query.getOffset());
sq.setRows(query.getLimit());
sq.set("collection", query.getCollection());
try (QueryResponse rsp = solrClient.query(sq)) {
SolrDocumentList docs = rsp.getResults();
QueryResult result = new QueryResult();
result.setTotal((int) docs.getNumFound());
result.setDocs(convertDocs(docs));
return result;
} catch (SolrServerException | IOException e) {
throw new IndexingException(e);
}
}
private List convertDocs(SolrDocumentList docs) {
List list = new ArrayList<>(docs.size());
for (SolrDocument sd : docs) {
Doc d = new Doc();
sd.forEach((k, v) -> d.addField(k, v));
list.add(d);
}
return list;
}
// ========== 優雅關閉 ==========
@Override
public void destroy() throws Exception {
shutdown(SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS);
}
public boolean shutdown(long timeout, TimeUnit unit) {
if (!shuttingDown.compareAndSet(false, true)) {
log.info("重複關閉請求被忽略");
return false;
}
String baseName = updateThread.getName();
updateThread.setName(baseName + "-SHUTTING_DOWN");
// 發送關閉信號
CountDownLatch latch = new CountDownLatch(1);
inputQueue.add(new Operation(OpType.SHUTDOWN, latch));
try {
if (timeout > 0) {
updateThread.setName(baseName + "-WAITING");
boolean completed = latch.await(timeout, unit);
if (!completed) {
log.warn("關閉超時,強制中斷");
}
return completed;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("關閉等待被中斷", e);
} finally {
running.set(false);
updateThread.setName(baseName + "-STOPPED");
}
return true;
}
// ========== 內部數據結構 ==========
enum OpType { UPDATE, DELETE, SHUTDOWN }
static class Operation {
final OpType type;
final Indexable indexable;
final CountDownLatch latch;
Operation(Indexable idx, OpType type) { this(idx, type, null); }
Operation(OpType type, CountDownLatch latch) { this(null, type, latch); }
Operation(Indexable idx, OpType type, CountDownLatch latch) {
this.indexable = idx; this.type = type; this.latch = latch;
}
}
// ========== 依賴接口定義(簡化) ==========
public interface Indexer {
void addIndex(Indexable idx) throws IndexingException;
void delIndex(Indexable idx) throws IndexingException;
}
public interface Searcher {
QueryResult search(Query q) throws IndexingException;
}
public interface Indexable {
String getCollection(); String getId(); Map getDoc();
}
public static class Doc { Map fields = new HashMap<>();
void addField(String k, Object v) { fields.put(k, v); }
}
public static class Query { /* 省略 */ }
public static class QueryResult { /* 省略 */ }
public static class IndexingException extends RuntimeException {
public IndexingException(Throwable cause) { super(cause); }
}
}
offer() 非阻塞,極快返回poll(1s) + drainTo() 批量收集collection + operation type 分組UpdateRequest.ACTION.COMMIT, false, false 軟提交(近實時)shuttingDown + running 狀態機| 參數 | 推薦值 | 說明 |
|---|---|---|
| QUEUE_CAPACITY | 16384 ~ 65536 | 根據堆內存、峰值 QPS 調整 |
| BATCH_INTERVAL_MS | 500 ~ 2000 | 延遲 vs 吞吐權衡 |
| SolrClient | HttpSolrClient / CloudSolrClient | 連接池、重試、熔斷配置 |
| 軟提交 | openSearcher=false, softCommit=true | 近實時,不打開新搜索器 |
| 硬提交 | 定時任務每 15-60 分鐘 | 確保數據持久化、段合併 |
RetryTemplate 或死信隊列本 SolrIndexer 實現了「高吞吐、低延遲、可靠關閉」的三大目標:BlockingQueue 解耦生產消費,批量聚合減少 RPC,CountDownLatch 保證關閉時數據零丟失。直接可用於電商商品索引、日誌檢索、內容檢索等高並發場景,並可平滑遷移至 SolrCloud 或 Elasticsearch 架構。