ThinkPHP 8.0中锁表不适用于秒杀防超卖,应使用InnoDB行级悲观锁+事务+lock(true),配合索引命中;lock(false)为表级写锁,导致全表阻塞,QPS骤降;标准做法是事务内where后调用lock(true)触发SELECT...FOR UPDATE,并加stock>=num二次校验。

ThinkPHP 8.0 中“锁表”不是秒杀场景下防超卖的合理选择,真正有效的是行级悲观锁 + 事务,配合 InnoDB 引擎与索引命中。所谓“锁表”(如 lock(false) 或 LOCK TABLES)会导致全表阻塞,QPS 断崖式下跌,完全不适用于高并发秒杀。
为什么不能用 lock(false) 锁表?
ThinkPHP 的 lock(false) 底层执行的是 MySQL 的 LOCK TABLES xxx WRITE,属于表级写锁:
- 所有读写请求(包括其他商品查询)都会排队等待,哪怕只操作一个商品 ID
- 在秒杀峰值下,单表锁会让整个库存模块串行化,吞吐量归零
- TP6.1+ 已明确废弃字符串锁(如
lock('update')),行为不可控且不生效
正确做法:InnoDB 行锁 + 事务 + lock(true)
lock(true) 不是自动加锁,它只有在事务内、配合 where() 和 find() 时,才会生成 SELECT ... FOR UPDATE 语句,从而对目标行加排他锁:
- 必须使用 InnoDB 表引擎,且 where 条件字段(如
id)要有索引,否则可能升级为表锁 - 锁必须在
find()执行时触发,放在update()前无效——因为 update 不走 select,锁不住 - 事务需显式开启,推荐用
Db::transaction()包裹整段逻辑
秒杀扣库存的标准代码结构
以商品 ID 为 1001、需扣减 1 件为例:
立即学习“PHP免费学习笔记(深入)”;
Db::transaction(function () use ($goodsId, $num) {
// 关键:lock(true) 必须紧跟 where() 后、find() 前
$goods = Db::name('goods')
->where('id', $goodsId)
->lock(true)
->find();
<pre class="brush:php;toolbar:false;">if (!$goods || $goods['stock'] < $num) {
throw new Exception('库存不足');
}
// 原子更新:WHERE 条件确保不会扣成负数
$result = Db::name('goods')
->where('id', $goodsId)
->where('stock', '>=', $num) // 防超卖二次校验
->update(['stock' => Db::raw("stock - {$num}")]);
if (!$result) {
throw new Exception('库存已被抢完');
}});
注意:where('stock', '>=', $num) 是必要兜底,防止锁失效或网络重试导致重复扣减。
更高并发场景建议叠加 Redis 预扣
纯数据库行锁适合中等并发(几百~几千 TPS)。若需支撑万级 QPS,应分层处理:
- 秒杀开始前,用
LPUSH goods:1001 1将 100 个占位符推入 Redis List - 用户请求时,直接
LPOP goods:1001—— 成功即获得资格,天然原子防超卖 - 预扣成功后,再异步进队列做最终下单和 DB 库存落库,DB 层仍需用上述行锁逻辑保障最终一致性



















