优先用数据库但必须加Redis缓存层:先INCR计数,再定时批量落库;去重用“IP+UA前32字符+栏目ID+日期”组合键SETNX;服务端兜底统计防漏报;查数据走预聚合宽表而非实时聚合。

直接记录访问日志还是写数据库更合适
栏目访问量统计本质是「高频写入 + 低频聚合」,PHP 每次 file_put_contents() 追加日志或每次 INSERT INTO 写库都可能成为瓶颈。真实项目里,优先用数据库但必须加缓存层:先写内存(如 Redis 的 INCR),定时(比如每分钟)批量落库。否则高并发下 MySQL 容易锁表,尤其栏目页被爬虫扫或活动引流时。
- 纯文件日志适合单机小站,但
fopen(..., 'a')在 NFS 或容器环境可能丢数据 - MySQL 直写必须给
category_id加索引,否则SELECT SUM(count) FROM stats WHERE category_id = ?会全表扫描 - Redis 方案示例:
$redis->incr("stat:cat:{$catId}"),再用CRON脚本每 60 秒执行一次GET+DEL+INSERT
怎么避免重复统计(同一用户刷多次)
靠 $_SERVER['HTTP_USER_AGENT'] 或 IP 做去重极不可靠:内网共用出口 IP、手机网络切换、浏览器隐私模式都会失效。更务实的做法是组合判断:「IP + User-Agent 前 32 字符 + 栏目 ID + 当天日期」拼成唯一键,用 Redis SETNX 控制 24 小时内只记一次。不推荐用 Cookie,因为栏目页常被分享到微信/微博,打开即无上下文。
- 不要用
session_start()后读$_SESSION,栏目页通常禁用 session(影响 CDN 缓存) - 时间窗口设为 86400 秒(1 天),而非按自然日,避免跨零点并发写冲突
- 如果必须精确到小时,键名改成
"stat:cat:{$catId}:{$hourStamp}",$hourStamp = date('YmdH')
前端异步上报会不会漏统计
用 JS 发 fetch('/api/stat?cat=123') 看似优雅,但页面跳转快、用户关闭标签、AdBlock 拦截、CSP 策略都会导致请求发不出。真实线上数据表明,纯前端上报的漏报率常超 15%。必须服务端兜底:在栏目页 PHP 模板最底部嵌一段 file_get_contents() 或 cURL 调用统计接口(注意设 timeout=1 防阻塞),前端上报只作补充。
- 服务端调用要加
@抑制错误,且不能等响应,cURL 需设CURLOPT_TIMEOUT_MS => 100 - 前端 fetch 应放在
window.addEventListener('beforeunload', ...)里补发一次,但别依赖它 - 统计接口本身要幂等:重复请求相同
cat_id和date不应多计
统计结果怎么查才不拖慢前台页面
栏目页加载时实时查总访问量?千万避免。正确做法是:后台任务每 5 分钟把各栏目汇总值写进一张宽表 category_stats_daily,字段包括 category_id、stat_date、pv_total、uv_total。前台页面直接 SELECT pv_total FROM category_stats_daily WHERE category_id = ? AND stat_date = CURDATE() —— 单行查询,毫秒级返回。
立即学习“PHP免费学习笔记(深入)”;
- 宽表不要用
GROUP BY实时聚合原始日志表,那张表只做归档,不参与前台查询 - 如果需要「今日实时 PV」,单独建
category_stats_realtime表,用REPLACE INTO每分钟覆盖更新 - Redis 缓存也得设:
GET "stat:cat:{$catId}:today",未命中再查库并回填,TTL 设 60 秒防雪崩
try/catch,且 catch 里只打日志,不抛异常。











