ThinkPHP文章浏览量统计应使用inc()方法实现数据库原子自增,避免PHP端计算导致NULL+1错误及并发漏计;需配合IP/UA去重或Session/Cookie防重复,并确保字段默认值为0。

为什么直接用 $article->save(['view_count' => $article->view_count + 1]) 会出错
因为 ThinkPHP5 的模型在读取数据后,view_count 字段若为 null(比如新文章还没设初始值),null + 1 在 PHP 中结果是 1,但 MySQL 里 NULL + 1 是 NULL,导致实际写入数据库的是 NULL,刷新后还是 0。更隐蔽的问题是:并发请求时多个请求同时读到同一个旧值,各自加 1 后写回,造成漏计数。
解决思路不是靠 PHP 算术累加,而是交由数据库原子操作:
- 确保数据库字段默认值设为
0(ALTER TABLE article MODIFY view_count INT DEFAULT 0) - 改用
Db::execute()或模型的inc()方法触发 SQL 的SET view_count = view_count + 1 - 避免先查再改,杜绝并发覆盖
用 inc() 方法安全自增(推荐)
inc() 是 ThinkPHP5 模型内置的原子递增方法,底层生成 UPDATE ... SET field = field + 1,不依赖当前 PHP 值,天然防并发。但要注意调用方式和条件:
- 必须基于主键或明确
where条件调用,例如:Article::where('id', $id)->inc('view_count')->update() - 不能对空模型实例直接调用
$article->inc('view_count'),否则会报错“没有指定更新条件” - 如果需要同时更新其他字段(如
last_view_time),要合并进同一个update(),不要拆成两次操作 - 返回值是影响行数(通常为 1),可据此判断是否更新成功
示例代码:
立即学习“PHP免费学习笔记(深入)”;
Article::where('id', $id)
->inc('view_count')
->update(['last_view_time' => date('Y-m-d H:i:s')]);
如何避免重复计数(比如刷新、爬虫、前端预加载)
单纯每次访问就加 1,会导致真实浏览量虚高。常见过滤手段有:
- 用 Session 标记:用户首次访问该文章时才自增,
session('viewed_'.$id)存在则跳过(注意 Session 生命周期和清理) - 用 Cookie 配合时间窗口:设置
cookie('viewed_'.$id, 1, 86400),24 小时内同一设备只计一次 - 服务端 UA + IP + URL 组合去重(仅限后台分析,不适合高频写入场景)
- 前端埋点控制:把计数逻辑放在文章主体渲染完成后的 JS 里,避开预渲染、RSS 抓取等非人工访问
注意:Session/Cookie 方案对无痕模式、多设备用户不生效,需按业务容忍度权衡。
为什么不用事务包裹查询+更新
有人想先 select 再 update 并套上事务,这在 TP5 里反而更危险:
- 事务不能防止 SELECT 之后其他请求抢先 UPDATE(除非加
SELECT ... FOR UPDATE,但 TP5 默认不支持行锁语法) - TP5 的
startTrans()对普通查询无锁效果,只是保证语句原子提交 - 增加数据库连接占用,高并发下易触发连接池耗尽
- 代码变复杂,却没解决根本问题
真正需要强一致性计数(如抢购库存),应换用 Redis 的 INCR 做前置计数,再异步落库;普通文章浏览量,inc() 已足够可靠。
关键点始终是:别让 PHP 做数值计算,让数据库做;别依赖“先读后写”,用原子指令代替。



















