thinkphp 5.1中需用事务+select for update行锁解决库存超卖:先starttrans()开启事务,再lock(true)查询锁定目标行,判断库存后update扣减,最后commit()释放锁;务必确保where条件命中主键或唯一索引,避免升级为表锁。

在ThinkPHP 5.1中处理高并发库存扣减时,若未加锁直接查+改,两个请求可能同时读到相同库存值(如10),各自扣1后都写回9,造成超卖——这就是典型的脏读问题,必须用数据库行级锁阻断并发读取路径。
用 lock(true) 开启 SELECT FOR UPDATE 行锁
第一步:在事务内执行查询时显式加写锁,强制后续请求等待当前事务释放锁。
第二步:调用模型的 【lock(true)】 方法,它会生成带有 SELECT ... FOR UPDATE 的 SQL,而非普通 SELECT。
第三步:确保该查询处于事务中,否则 FOR UPDATE 无效或被自动提交——TP5.1 中必须先调用 $model->startTrans()。
这一步不能省略:如果只写 $model->lock(true)->find() 但没开启事务,MySQL 会把它当作自动提交语句执行,锁在语句结束瞬间释放,起不到串行保护作用。
组合事务 + 行锁完成原子扣减
方法一:手动控制事务流程
① 调用 $model->startTrans() 显式开启事务;
② 执行 $goods = $model->lock(true)->where('id', $id)->find(),此时该记录被锁定,其他并发请求在此处阻塞;
③ 判断 $goods['stock'] >= $num,不满足则 $model->rollback() 并抛异常;
④ 满足则执行 $model->where('id', $id)->update(['stock' => $goods['stock'] - $num]);
⑤ 成功后调用 $model->commit() 提交并释放锁。
注意:第④步 update 不再需要 lock(true),因为行锁由上一步 find 持有,直到 commit 或 rollback 才释放。
避免锁表陷阱:只锁目标行,不锁整张表
直接使用 DB::table('goods')->lock(true)->where(...)->select() 是安全的,MySQL 默认对 WHERE 条件命中行加行锁;
但若 WHERE 条件未命中索引(例如用非主键、无索引字段查询),InnoDB 会升级为间隙锁甚至表锁,导致整个商品表被堵死;
【务必确认 id 字段已建主键或唯一索引】,否则 lock(true) 实际效果等同于锁表,吞吐量断崖下跌。
这步验证不能跳过:用 EXPLAIN SELECT * FROM goods WHERE id = 123 FOR UPDATE 查看 type 是否为 const 或 eq_ref,如果不是,立刻补索引。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











