首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java 多線程異步構建 Solr 索引:生產級佇列+批量提交+優雅關閉

     編輯:Java 程式設計入門

前言

高吞吐場景下,單線程同步調用 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); }
    }
}

關鍵設計亮點解析

1. 生產消費解耦:BlockingQueue

  • 業務線程 offer() 非阻塞,極快返回
  • 隊列容量 16384,防止內存暴漲(可配置)
  • 後台線程 poll(1s) + drainTo() 批量收集

2. 批量聚合提交:減少網絡往返

  • 按 collection + operation type 分組
  • 單次 HTTP 請求提交百/千文檔
  • 使用 UpdateRequest.ACTION.COMMIT, false, false 軟提交(近實時)

3. 優雅關閉:CountDownLatch 同步

  • 發送 SHUTDOWN 操作進隊列,等待消費完成
  • 超時強制返回,防止無限阻塞
  • 雙重標記 shuttingDown + running 狀態機

4. 異常隔離:不影響後續批次

  • 單批次失敗記錄錯誤,繼續處理下一批
  • 關閉過程中異常降級為 WARN

性能調優建議

參數推薦值說明
QUEUE_CAPACITY16384 ~ 65536根據堆內存、峰值 QPS 調整
BATCH_INTERVAL_MS500 ~ 2000延遲 vs 吞吐權衡
SolrClientHttpSolrClient / CloudSolrClient連接池、重試、熔斷配置
軟提交openSearcher=false, softCommit=true近實時,不打開新搜索器
硬提交定時任務每 15-60 分鐘確保數據持久化、段合併

擴展方向

  1. 多消費線程:按 collection 分片,多線程並行消費
  2. 失敗重試:引入 RetryTemplate 或死信隊列
  3. 監控指標:Micrometer 統計隊列積壓、批次耗時、錯誤率
  4. 幂等性:文檔帶版本號,支持樂觀鎖更新
  5. 分布式協調:SolrCloud 下結合 ZK 實現領導者選舉

總結

本 SolrIndexer 實現了「高吞吐、低延遲、可靠關閉」的三大目標:BlockingQueue 解耦生產消費,批量聚合減少 RPC,CountDownLatch 保證關閉時數據零丟失。直接可用於電商商品索引、日誌檢索、內容檢索等高並發場景,並可平滑遷移至 SolrCloud 或 Elasticsearch 架構。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved