TP5库存逻辑核心是将扣减与业务校验、事务、锁绑定在模型或服务层,而非裸用setDec();因setDec()无前置校验,易致超卖;须两次查询+FOR UPDATE行锁+事务保障原子性;Redis队列适用于高并发秒杀场景。

TP5 自定义函数写库存逻辑,核心不是“封装一个函数”,而是把库存变更操作和业务约束绑定在模型层或服务层,避免裸写 SQL 或直接调用 setInc()/setDec() 导致并发、越界、事务丢失等问题。
为什么不能直接在控制器里用 setDec() 减库存
常见错误现象:用户秒杀时库存变负数、两个请求同时读到 1 库存,都执行 setDec('stock', 1),结果库存变成 -1。
根本原因:setDec() 是原子操作,但「判断是否够减」这步不在原子范围内。它只做「无条件减」,不校验当前值。
使用场景中必须前置校验的环节:
- 商品是否上架(status = 1)
- 库存是否 ≥ 要减的数量
- 是否已下单/锁单(防重复提交)
- 扣减后是否触发预警阈值(如 ≤5)
推荐做法:在模型里封装带校验的库存扣减方法
以 ProductModel 为例,在 application/common/model/Product.php 中添加:
public function decStock($id, $num = 1)
{
// 先查当前库存和状态
$product = $this->field('id, stock, status')->find($id);
if (!$product || $product['status'] != 1) {
return ['code' => 0, 'msg' => '商品不可用'];
}
if ($product['stock'] < $num) {
return ['code' => 0, 'msg' => '库存不足'];
}
<pre class="brush:php;toolbar:false;">// 开启事务,确保查+减原子性
Db::startTrans();
try {
// 再次用 for update 锁住该行(防止并发查到旧值)
$row = Db::name('product')->lock(true)->where('id', $id)->find();
if ($row['stock'] < $num) {
throw new \Exception('库存已被抢完');
}
Db::name('product')->where('id', $id)->setDec('stock', $num);
Db::commit();
return ['code' => 1, 'msg' => '扣减成功', 'left' => $row['stock'] - $num];
} catch (\Exception $e) {
Db::rollback();
return ['code' => 0, 'msg' => $e->getMessage()];
}}
关键点:
- lock(true) 触发 SELECT ... FOR UPDATE,是 MySQL 行锁保障
- 两次查询非冗余:第一次快速判断,第二次加锁后精确校验
- setDec() 在事务内执行,失败可回滚
- 返回结构统一,便于控制器直接响应
自定义助手函数仅适合简单计数类场景
如果你只是做「文章阅读量 +1」这种无业务约束的累加,可以写全局助手函数:
// application/common.php
function inc_view_count($table, $id)
{
return Db::name($table)->where('id', $id)->setInc('view_count', 1);
}但注意:
- 不适用于库存,因为缺少校验与事务
- inc_view_count('article', 123) 可用,inc_view_count('product', 456) 不可用
- 函数名要体现语义,避免叫 updateStock() 这种模糊命名
Redis 队列方案更适合高并发库存(如秒杀)
当数据库扛不住大量 SELECT ... FOR UPDATE 时,需降级为预扣减:
- 商品上架时,把库存数量推入 Redis 列表:LPUSH goods_1001 1(推 N 次)
- 下单时用 LPOP goods_1001,成功即代表库存可用
- 成功后才写订单、再异步更新 MySQL 库存(最终一致性)
此时自定义函数应封装 Redis 操作,而非 DB 操作:
- 不依赖 Db::,改用 Cache::store('redis')->lPop()
- 必须设置超时(EXPIRE goods_1001 3600),防脏数据堆积
- 后台需有定时任务核对 Redis 与 DB 库存是否一致
真正容易被忽略的是:库存变更从来不是单点操作,它必然牵扯订单生成、日志记录、消息通知、预警推送。把「扣库存」写成一个孤立函数,等于把地基建在沙子上。

















