不能直接在控制器里裸用setinc统计访问量,因缺乏ip/session去重致uv/pv混淆、高并发下读改写非原子导致丢数、无缓存缓冲易压垮mysql写连接池。

不能直接在控制器里用 setInc 更新数据库字段来记访问记录,否则 UV/PV 混乱、高并发丢数、MySQL 写崩是大概率事件。
为什么 setInc 不能裸用在页面统计里
很多人在 detail() 方法里写 $article->where(['id'=>$id])->setInc('view_count'),这会立刻暴露三个问题:
- 没做去重:F5 刷新一次就 +1,同一用户一分钟点五次,UV 算成 5,实际是 1
- 没防并发:两个请求几乎同时读出 100,各自 +1 再写回,最终变成 101 而不是 102
- 没缓冲:QPS 过 200 就可能压垮 MySQL 的写连接池,尤其当表没加合适索引或没用
INSERT ... ON DUPLICATE KEY UPDATE
ThinkPHP6 推荐的中间件 + 缓存 + 定时落库流程
核心是把「计数行为」和「落库动作」解耦。中间件只负责压缓存,后台命令定时批量刷库,不阻塞响应。
- 缓存键建议带前缀和哈希:
cache('pv_' . md5($request->url()), null, 3600),避免/article/1和/article/1?from=weibo被当成不同页面 - UV 统计必须依赖 Cookie 或 Session ID,
$_SERVER['REMOTE_ADDR']在 NAT/CDN 下完全不可靠 - 定时任务用
php think count:sync,每 5 分钟执行一次,批量读取所有pv_*键,用Db::name('page_pv')->insertAll(...)+ON DUPLICATE KEY UPDATE合并写入 - 不要用
file_get_contents+file_put_contents模拟缓存,TP6 的 FileCache 在 FPM 多进程下无跨进程锁,必然丢数
数据库表结构怎么设计才不翻车
一张表硬扛所有维度,不出三个月就会查不动。真实可用的结构至少分三张:
-
site_pv:总 PV 按天聚合,字段为date DATE、count BIGINT,联合主键或唯一索引(date) -
page_pv:页面级 PV,字段含url_hash VARCHAR(32)(存md5($request->url()))、date、count,联合唯一索引(url_hash, date) -
uv_daily:独立访客,字段含visitor_id CHAR(32)(Session ID 或设备指纹)、date、url_hash,联合唯一索引(visitor_id, url_hash, date),靠唯一约束自然去重
注意:HTTP_REFERER 和 HTTP_USER_AGENT 必须过滤长度(至少 VARCHAR(512))并转义,否则入库时静默截断或 SQL 注入风险极高。
最容易被忽略的细节:Referer 和 UA 的处理
这两项看似只是“记录来源”,但实际是高频出错点:
-
$_SERVER['HTTP_REFERER']可能为空、可能超长(>1000 字符)、可能含换行或单引号,直接拼 SQL 会报错或被注入 -
$_SERVER['HTTP_USER_AGENT']常见长度 300–400 字符,VARCHAR(255)会静默截断,查日志时发现“Chrome”变“Chrome…”,根本没法分析 - TP6 推荐统一走
$request->header('referer')和$request->header('user-agent'),再用mb_substr(..., 0, 512)截断 +htmlspecialchars()转义
缓存键名、数据库字段长度、输入过滤——这三个地方只要漏一个,上线后查数据时就会发现“统计数对不上”“UV 比 PV 还高”“某天突然少了 80% 记录”。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











