頁面訪問量(PV)統計看似簡單,實則是高併發系統的經典難題。本文復盤一個真實項目:從最初的同步寫庫、單隊列阻塞,演進到多隊列分片、異步批量落庫、最終引入 JS 異步埋點的完整優化歷程。
request.hashCode() % N 分發到 N 個 LinkedList,每個隊列獨立消費。<script> 異步上報,完全剝離服務端壓力。
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;
}
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);
}
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();
}
}
}
}
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 異步上報 | 業務請求零額外開銷 |
<script>
(function() {
var img = new Image();
img.src = '/stat.gif?uri=' + encodeURIComponent(location.pathname) + '&t=' + Date.now();
})();
</script>
優勢:完全非阻塞、不佔用服務端線程、支援 CDN、天然適配前後端分離架構。
完整項目代碼已開源於 GitHub,歡迎 Star 交流。