必须加锁,否则多请求并发读写计数文件会因竞态导致数字丢失;flock可避免此问题,推荐用’c+’模式打开并直接lock_ex加锁,路径须避开web可访问目录,写入后及时fflush+fclose。

用 fopen + flock 写文件计数器,为什么必须加锁
不加锁时多个请求同时读写同一个计数文件,大概率导致数字“丢失”——比如两个请求都读到 100,各自加 1 后都写回 101,实际 PV 少算 1 次。flock 是 php 原生支持的轻量级文件锁,能避免竞态。
实操建议:
- 用
FILE_APPEND | LOCK_EX模式打开文件,而不是先fopen再flock,减少中间窗口 - 计数文件路径别放在 Web 可访问目录下(如
/var/www/html/counter.dat),推荐放/tmp/counter.dat或项目外独立路径 - 写入后立即
fflush+fclose,避免系统缓存延迟落盘 - 示例片段:
$fp = fopen('/tmp/counter.dat', 'c+'); if (flock($fp, LOCK_EX)) { $count = (int) fgets($fp, 1024); fseek($fp, 0); ftruncate($fp, 0); fwrite($fp, (string)($count + 1)); fflush($fp); fclose($fp); }
mysql 存 PV 用 INSERT ... ON DUPLICATE KEY UPdate 还是直接 UPDATE
如果按天统计(如表结构含 date + pv 字段),且 date 是唯一键,INSERT ... ON DUPLICATE KEY UPDATE 更安全:首次写入自动插入,重复则更新,不用先 select 再判断再 INSERT/UPDATE,省一次查询、避竞态。
但要注意:
立即学习“PHP免费学习笔记(深入)”;
- 必须给
date字段建UNIQUE索引,否则ON DUPLICATE KEY不生效 - 不要用
REPLACE INTO,它本质是删+插,在有外键或自增 ID 场景下行为不可控 - 高并发下,即使用了
ON DUPLICATE KEY,仍建议把 SQL 包在事务里(尤其要连带更新 UV 时) - 如果只统计总 PV 不分日期,直接
UPDATE counter SET pv = pv + 1 WHERE id = 1最简单,无需事务
为什么不能直接用 $_SERVER['REMOTE_ADDR'] 做 UV 统计
$_SERVER['REMOTE_ADDR'] 在有 CDN、反向代理(如 nginx)时,返回的是代理 IP,不是真实访客 IP,会导致 UV 严重偏低甚至全为同一个 IP。
正确做法是优先读取 HTTP_X_forWARDED_FOR 或 HTTP_X_REAL_IP,但必须校验可信来源:
- 只信任你自己的 CDN 或代理 IP 段(如 Nginx 配置了
set_real_ip_from 192.168.1.0/24;),否则X-Forwarded-For可被伪造 - PHP 中需配合
real_ip_header和real_ip_recursive配置(PHP 7.3+),或手动解析并验证 IP 格式与可信段 - 更稳妥的 UV 方案是用 cookie + IP 组合去重,或直接依赖前端埋点 + 后端日志聚合(如用 Nginx 日志 + Logstash)
redis 计数器比文件快,但要注意 INCR 的原子性和持久化配置
INCR 本身是原子操作,不用额外加锁,适合高并发 PV 统计;但默认 Redis 是内存型,断电即丢数据,PV 会归零。
权衡建议:
- 若接受少量丢失(如只关注当日趋势),用
INCR counter:today足够,性能碾压文件和 DB - 如需强一致性,开启
AOF(appendonly yes)并设appendfsync everysec,兼顾性能与安全性 - 避免用
GET + INCR手动实现,这会破坏原子性;也别用INCRBYFLOAT统计整数 PV - 记得给 key 加过期时间(如
EXPIRE counter:today 86400),否则长期累积无清理
事情说清了就结束。真正上线时,文件方案容易被忽略锁机制,数据库方案常漏掉唯一索引,Redis 方案最常踩的坑是没配 AOF 还以为“绝对不丢”。