前言:为什么要自建统计接口
在个人站点建设中,PV(访问量)和 UV(独立访客数)统计虽然看似简单,却对数据分析和运营决策至关重要。
最开始,我使用 Busuanzi 官方接口,只需嵌入几行代码即可显示 PV/UV:
- 简单易用,无需服务器维护
- 即刻可显示访问量和访客数
然而,随着访问量增加,Busuanzi 的问题逐渐显现:
- 接口偶尔挂掉,导致统计数据无法显示
- 加载速度不稳定,影响页面响应体验
- 数据不可控,无法长期保存和分析历史访问情况
为了保证数据的可用性、稳定性和自主可控,我尝试自建统计接口:
- PV:每次访问页面自增
- UV:按 IP 去重,仅在新访客时增加
- JSONP 输出,保持与 Busuanzi 接口兼容
通过自建接口,我可以完全掌控统计数据,同时避免依赖第三方服务的不稳定性。
自建统计接口的尝试与教训
最初,我在自己的服务器上搭建统计接口:
- PV:每次访问页面自增
- UV:按 IP 去重,仅在新访客时增加
短期内效果不错,但 服务器到期未续费,接口停止运行,之前累积的统计数据全部丢失。
这就是本网站统计数据清零的主要原因:由于自建服务器停止运行,所有原有数据无法迁移到新系统,只能从零重新累积。
这个经历也提醒我:
统计系统不仅要能计算 PV/UV,更需要 可靠存储和高可用,否则数据随时可能丢失。
迁移到 EdgeOne KV
为了提升可用性,同时避免依赖单台服务器,我将统计逻辑迁移到了 EdgeOne KV。
KV 优势
- 分布式高可用,不再依赖单台服务器
- 低延迟访问,适合实时 PV/UV 统计
- 按 key 分组管理,便于全站和单页面统计
- 数据可长期保存,解决了自建服务器丢失数据的问题
统计逻辑
- PV(访问量)
- UV(独立访客数)
- 全站总访客数(去重 IP)
- 当前页面总访客数(去重 IP)
- 输出 JSONP,保持与 Busuanzi 接口兼容
性能优化策略
- 并行读取 KV
- 使用
Promise.all 同时获取 PV/UV
- 异步写入 PV/UV
- PV 写入不阻塞响应
- UV 仅在新 IP 时写入 KV
- 去重处理
- 全站 UV:记录访问过 IP
- 页面 UV:记录访问过页面的 IP
经过优化,响应时间从 约 898ms 降至 约 220ms。
从零开始重建统计
由于之前自建接口服务器停止,原有统计数据丢失,本网站数据全部 从零开始:
- 全站 PV/UV 从零累积
- 每个页面 PV/UV 也从零累积
- 可扩展按天统计,定期清理超过 7 天的历史数据
清零原因已经明确:服务器停止导致原数据无法迁移到 KV,所以新的统计系统从零开始累积。
EdgeOne 免费版配额概览(30 天)
| 功能 |
配额上限 |
| 安全加速流量 |
不限量 |
| 安全加速请求数 |
不限量 |
| Edge Functions 请求数 |
300 万 |
| Edge Functions CPU 时间 |
300 万秒 |
| Cloud Functions 请求数 |
100 万 |
| Cloud Functions GBs |
50 万 |
| KV 存储 |
1 GB |
| 构建次数 |
500 |
个人站点使用完全够用,KV 存储可保存长期 UV/IP 数据。
Edge KV 核心实现代码
下面是实际使用的 EdgeOne KV PV/UV 统计接口代码,保持与 Busuanzi JSONP 接口兼容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| export async function onRequestGet({ request }) { const KV = stats_kv;
const url = new URL(request.url); const callbackRaw = url.searchParams.get("jsonpCallback") || "BusuanziCallback"; const callback = callbackRaw.replace(/[^a-zA-Z0-9_]/g, "");
const referer = request.headers.get("Referer") || "https://unknown_site/"; const refererUrl = new URL(referer, "https://unknown_site"); const siteId = refererUrl.hostname.replace(/[^a-zA-Z0-9_\-]/g, "_"); const rawPath = refererUrl.pathname || "/"; const pagePath = rawPath.replace(/[^a-zA-Z0-9_\-./]/g, "_") || "_index";
const clientIP = request.eo?.clientIp || request.headers.get("EO-Client-IP")?.split(",")[0].trim() || `test_${Math.floor(Math.random() * 100000)}`;
const [ sitePVVal, pagePVVal, siteUVVal, siteIPVisited, pageUVCountVal, pageIPVisited, ] = await Promise.all([ KV.get(`site_pv_total:${siteId}`), KV.get(`page_pv_total:${siteId}:${pagePath}`), KV.get(`site_uv_total:${siteId}`), KV.get(`site_uv_alltime:${siteId}:${clientIP}`), KV.get(`page_uv_count:${siteId}:${pagePath}`), KV.get(`page_uv_total:${siteId}:${pagePath}:${clientIP}`), ]);
let sitePV = Number(sitePVVal) || 0; let pagePV = Number(pagePVVal) || 0; sitePV += 1; pagePV += 1;
KV.put(`site_pv_total:${siteId}`, String(sitePV)); KV.put(`page_pv_total:${siteId}:${pagePath}`, String(pagePV));
let totalUV = Number(siteUVVal) || 0; if (!siteIPVisited) { totalUV += 1; KV.put(`site_uv_alltime:${siteId}:${clientIP}`, "1"); KV.put(`site_uv_total:${siteId}`, String(totalUV)); }
let pageUV = Number(pageUVCountVal) || 0; if (!pageIPVisited) { pageUV += 1; KV.put(`page_uv_total:${siteId}:${pagePath}:${clientIP}`, "1"); KV.put(`page_uv_count:${siteId}:${pagePath}`, String(pageUV)); }
const data = { site_pv: sitePV, page_pv: pagePV, site_uv: totalUV, page_uv: pageUV, };
return new Response(`${callback}(${JSON.stringify(data)});`, { headers: { "Content-Type": "text/javascript; charset=UTF-8", "Access-Control-Allow-Origin": "*", }, }); }
|
总结
通过 EdgeOne KV 的 Serverless 架构,我实现了:
- 高可用、低延迟的全站和页面 PV/UV 统计
- 异步处理和并行 KV 请求,提高响应速度
- 从零开始重建统计系统,不依赖第三方接口
使用 KV 存储,你的统计数据掌握在自己手里,稳定可靠,适合个人或中小型网站。