ThinkPHP 的 inc 和 dec 方法非原子操作,高并发下会丢数据;必须配合数据库原生原子更新,显式调用 update() 并带 where 条件与业务校验(如 stock >= num),确保 SQL 为 SET field = field ± N 的单条原子语句。

ThinkPHP 的 inc 和 dec 方法不是原子操作
直接调用 inc 或 dec 更新字段(比如积分、库存)在高并发下会丢数据。原因很简单:TP 默认先查再改,中间有时间窗口。哪怕只差几毫秒,两个请求都读到同一初始值,各自加 1 后写回,结果只 +1 而非 +2。
实操建议:
- 必须配合数据库原生的原子更新,不能依赖 PHP 层的读-改-写流程
- 确认你用的是 ThinkPHP 6.x(5.x 类似但语法略有差异),且底层是 MySQL/PostgreSQL 等支持
UPDATE ... SET field = field + 1的数据库 - 避免在事务外裸用
inc()处理关键数值,比如订单库存扣减
正确写法:用 inc / dec 时强制走 SQL 原子表达式
ThinkPHP 的 inc 和 dec 方法本身支持传入第三个参数 $step,但关键在于——它必须作用于 update 场景,且不带 where 条件的链式调用默认不会生成原子 SQL;你要显式触发更新动作。
示例(TP6):
update()
是触发点,没有它,inc/dec 只是构建器状态,不发 SQL。
并发下的边界问题:WHERE 条件必须参与校验
光靠 inc 不够,你还得防止超扣。比如库存只剩 2,两个并发请求都想扣 3,都执行 dec('stock', 3) 就会变成 -1。
解决方法是在 where 中加入业务约束:
- 用
where('stock', '>=', $num)确保扣前足够 -
update()返回影响行数,为 0 表示条件不满足(如库存不足或记录已被删) - 不要依赖 PHP 层判断库存是否充足,那又回到非原子的老路
MySQL 在执行 UPDATE ... SET stock = stock - 3 WHERE id = X AND stock >= 3 时,整个条件+更新是一次加锁+计算,天然防超扣。
事务里用 inc/dec 要小心锁粒度
在事务中调用 inc + update(),本质是执行一条 UPDATE,InnoDB 会对匹配行加行锁(Record Lock)。但如果你的 WHERE 条件没走索引,可能升级为表锁或间隙锁,拖慢其他请求。
检查点:
- 确保
where字段(如id、order_no)有索引,否则并发更新同一张表的多条记录也可能互相阻塞 - 避免在事务里对同一行反复
inc多次再update—— 每次update()都是一次独立 SQL 和一次行锁获取 - TP6 的
inc不支持复合表达式(如score = score * 1.1),这种得手写Db::raw()
并发安全的核心不在 TP 方法名,而在你最终发出的那条 SQL 是否由数据库保证原子性。只要 inc/dec 落地为形如 SET field = field + N 的单条 UPDATE,并带上正确的 WHERE 校验和索引支撑,就站得住。不然,名字叫得再像原子操作也没用。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










