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

高併發頁面訪問統計方案:從單隊列到多隊列分片,再到異步批量落庫

     編輯:Java 綜合教學

高併發頁面訪問統計方案:從單隊列到多隊列分片,再到異步批量落庫

頁面訪問量(PV)統計看似簡單,實則是高併發系統的經典難題。本文復盤一個真實項目:從最初的同步寫庫、單隊列阻塞,演進到多隊列分片、異步批量落庫、最終引入 JS 異步埋點的完整優化歷程。

需求與挑戰

  • 記錄 IP、時間、URI 等可擴展訪問信息。
  • 零感知:不影響業務請求響應速度。
  • 支撐高併發(單機 200+ QPS 即崩潰的初版,優化後達 Tomcat 極限)。

演進路線圖

  1. 方案 1:同步寫庫 / ServletContext 計數器 —— 鎖競爭嚴重、DB 壓力大、不可用。方案 2:單 LinkedList 隊列 + 後台消費線程 —— 生產者放入隊列,消費者定期批量入庫。
    ⚠️ 問題:併發 200 時隊列積壓,消費線程假死,堆內存溢出 (OOM)。
  2. 方案 3:多隊列分片(Hash 分桶) —— 將請求按 request.hashCode() % N 分發到 N 個 LinkedList,每個隊列獨立消費。
    ✅ 顯著降低鎖競爭,支撐更高併發。
  3. 方案 4:JS 異步埋點(百度統計模式) —— 頁面嵌入 <script> 異步上報,完全剝離服務端壓力。

核心代碼實現(方案 3 精華)

1. 初始化:根據配置生成分片隊列數組

private static LinkedList<RequestStc>[] initUris() {
    // 解析 urls.xml 獲取需統計的 URI 集合
    int baseHash = uris.size() / 3;  // 分桶基數
    LinkedList<RequestStc>[] listArr = new LinkedList[baseHash];
    for (int i = 0; i < listArr.length; i++) listArr[i] = new LinkedList<>();
    return listArr;
}

2. 生產端:封裝請求並分桶入隊

public static void addCounter(HttpServletRequest request) {
    RequestStc stc = new RequestStc();
    stc.setIp(request.getRemoteAddr());
    stc.setUri(request.getRequestURI());
    stc.setNow(Calendar.getInstance().getTime());
    // 關鍵:hashCode 分桶,無鎖入隊
    buffers[request.hashCode() % baseHash].push(stc);
}

3. 消費端:輪詢各隊列,達閾值則克隆清空並異步落庫

private void processCount() {
    while (true) {
        Thread.sleep(SLEEP_MS);
        for (int i = 0; i < buffers.length; i++) {
            if (buffers[i].size() >= EXECUTE_BASE) {
                LinkedList<RequestStc> cloneList;
                synchronized (buffers[i]) {  // 僅鎖單個隊列
                    cloneList = (LinkedList<RequestStc>) buffers[i].clone();
                    buffers[i].clear();
                }
                new Thread(new ExecuteThread(cloneList)).start();
            }
        }
    }
}

4. ExecuteThread:批量落庫或聚合統計

public void flush() {
    // 方式 1:明細落庫(批量 INSERT)
    // 方式 2:聚合統計(Hashtable 維護 URL->PV 增量,定時 flush 到 DB)
    Hashtable<String, RequestParam> saveTable = (Hashtable<String, RequestParam>) counterTables.clone();
    counterTables.clear();
    for (Entry<String, RequestParam> ent : saveTable.entrySet()) {
        // 持久化邏輯
    }
}

關鍵優化點總結

痛點解決方案效果
單隊列鎖競爭Hash 分片多隊列鎖粒度從全局降至單桶,吞吐提升 10 倍+
消費線程假死導致 OOM克隆+清空模式,異步線程獨立處理主隊列不再積壓,內存穩定
頻繁小批量寫 DB閾值觸發 + 批量 INSERT / 聚合表DB 壓力降 90%+
Filter 攔截性能損耗JSP 內嵌調用 / JS 異步上報業務請求零額外開銷

JS 異步埋點終極方案

<script>
  (function() {
    var img = new Image();
    img.src = '/stat.gif?uri=' + encodeURIComponent(location.pathname) + '&t=' + Date.now();
  })();
</script>

優勢:完全非阻塞、不佔用服務端線程、支援 CDN、天然適配前後端分離架構。

總結與建議

  1. 統計系統核心是「生產-消費解耦」,隊列分片是高併發關鍵。
  2. 落庫採用「批量 + 聚合」雙模式,明細用於審計,聚合用於大屏展示。
  3. 最終演進至客戶端埋點,服務端僅提供輕量接收接口,架構最穩健。
  4. 監控指標:隊列長度、消費延遲、落庫耗時、異常率,建立告警體系。

完整項目代碼已開源於 GitHub,歡迎 Star 交流。

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