thinkphp点赞功能必须用数据库原子操作而非find()+save(),因后者存在“读-改-写”并发错误;推荐inc()实现跨库兼容的原子递增,配合联合唯一索引防重复点赞,并考虑异步落库提升性能。

ThinkPHP 的点赞功能不能靠 find() + save() 两次查询实现,否则高并发下计数必然错乱;必须用数据库原生的原子操作,且要区分 MySQL 和 PostgreSQL 的语法差异。
为什么 update(['likes' => $likes + 1]) 不安全?
这是最典型的“读-改-写”陷阱:两个请求同时查出 likes = 10,各自加 1 后都写入 11,实际应为 12。ThinkPHP 的模型层默认不提供自动重试或行锁封装,单纯靠 PHP 层加锁(如 flock)会严重拖慢响应,也不适合分布式部署。
真正可靠的解法是把“加 1”动作下推到数据库执行:
- MySQL:用
SET likes = likes + 1配合where条件更新 - PostgreSQL:语法相同,但需注意字段名是否带双引号(尤其含大写字母时)
- SQLite:支持
+=语法,但 ThinkPHP 6+ 的inc()方法底层已适配
Db::raw('likes + 1') 和 inc('likes') 怎么选?
inc() 是 ThinkPHP 封装的原子递增方法,语义清晰、兼容多库,但仅适用于整型字段的简单自增;Db::raw() 更灵活,可用于复合表达式(如 likes + CASE WHEN is_vip THEN 2 ELSE 1 END),但需手动拼 SQL,丧失部分安全性。
日常点赞场景推荐用 inc():
// 正确:一行完成原子 +1
Db::name('article')->where('id', 123)->inc('likes')->update();
// 错误:先查后更,非原子
$article = Db::name('article')->find(123);
Db::name('article')->where('id', 123)->update(['likes' => $article['likes'] + 1]);
注意:inc() 返回的是影响行数(int),不是新值;如需返回更新后的 likes,得额外查一次或用 RETURNING(PostgreSQL)/ 触发器(MySQL)。
防止重复点赞的关键在业务层,不在数据库
数据库原子操作只保证“加 1 不丢”,不解决“用户重复点”。必须在点赞前检查该用户是否已点过,典型做法是建联合索引表:
- 表
user_like:字段user_id、target_id、target_type(如 'article') - 联合唯一索引:
(user_id, target_id, target_type) - 插入前用
insertGetId()或捕获IntegrityConstraintViolationException
不要用“查是否存在 → 不存在则插入”两步,中间仍有竞态窗口;直接 INSERT IGNORE(MySQL)或 ON CONFLICT DO NOTHING(PostgreSQL)更稳妥。
计数器要不要实时更新?
对阅读量、点赞数这类非强一致性指标,可考虑异步化:前端点击后立即返回成功,后台用消息队列(如 Redis List + Worker)延迟 1–3 秒再落库。这样既防刷,又避免 DB 成为瓶颈。
但要注意两点:
- Redis 缓存计数器时,务必用
INCR命令,而非GET + SET - 定时任务同步 Redis 到 DB 时,要用
GETSET或 Lua 脚本确保读写原子性,否则可能漏数
真正难的不是怎么写 inc(),而是想清楚“谁来判断可不可点”“点完要不要立刻写库”“写崩了怎么兜底”——这些逻辑一旦耦合进控制器,后期维护成本远高于 SQL 本身。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











