thinkphp无内置乐观锁,需用原子update+affectedrows判断:如db::name('goods')->where('id',123)->where('stock','>=',1)->update(['stock'=>db::raw('stock-1')]),返回0即失败;带版本号则需同时校验version字段并自增。

ThinkPHP 本身没有内置的乐观锁机制,save() 或 update() 方法不自动校验版本字段或库存前置条件——你写个 where('id', $id)->save(['stock' => $newStock]),就是裸更新,完全无法防超卖。
为什么 find() + save() 在 TP6/TP8 里不是乐观锁
这是最常被误用的“伪乐观锁”:先 find() 查出当前库存和版本号,再在 PHP 里计算新值,最后 save()。问题在于两步之间存在时间窗口,其他请求可能已修改该行。
- 哪怕你加了事务,
find()默认是快照读(MVCC),不加lock(true)就不会上行锁 -
save()的where条件若只传主键,等于放弃所有业务约束;若手动拼stock >= $oldStock,又会把变量当字面量注入,SQL 变成WHERE stock >= 99,而此时库存可能已被扣到 98 - 版本号字段(如
version)必须显式写进where条件,且 update 时同步自增,否则形同虚设
真正能用的乐观锁写法:原生 UPDATE + affectedRows 判断
必须把「读取、判断、扣减」压进一条原子 SQL,靠数据库返回影响行数决定成败。ThinkPHP 的查询构造器对 stock = stock - 1 这类自身字段运算支持有限,得用 Db::raw()。
- 基础安全写法:
Db::name('goods')->where('id', 123)->where('stock', '>=', 1)->update(['stock' => Db::raw('stock - 1')]),返回0表示条件不满足(库存不足或已被改) - 带版本号的写法:
Db::name('goods')->where('id', 123)->where('version', $expectedVersion)->update(['stock' => Db::raw('stock - 1'), 'version' => Db::raw('version + 1')]),同样依赖affectedRows === 1才算成功 - 注意:不要用
setDec('stock'),它底层仍是先查后更,无并发保护;也不要依赖模型的where()->save()链式调用,它的where不参与原子条件判断
TP8 中 lock(true) 是悲观锁,别和乐观锁混用
lock(true) 翻译为 SELECT ... FOR UPDATE,属于数据库悲观锁,会阻塞其他事务直到本事务提交。它和乐观锁是两种互斥思路,不能在一个流程里既加锁又搞版本号校验。
- 如果你用了
lock(true),后续update()就不需要再写stock >= $num条件——行锁已保证你读到的就是最新值,直接扣即可 - 但代价是并发性能下降,尤其在热点商品上容易排队等待,TP8 文档里那个
reduceStock示例走的就是这条路,适合一致性优先、QPS 不破千的场景 - 想用乐观锁,就必须放弃
lock(true)和事务中分步读写,全部收敛到单条UPDATE
版本号字段的初始值、更新时机、是否允许前端传入、失败后要不要重试——这些细节没对齐,乐观锁就只是多加了一列字段而已。真正的难点从来不在语法,而在业务语义与数据库原子能力的咬合精度。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











