tp8.0防超卖需redis原子预扣减(decrby)拦截95%请求,再以数据库乐观锁兜底校验;不可用get+decrby两步操作,因存在竞态窗口导致超卖。

直接用 redis()->decrBy() 扣减再查返回值,95% 的超卖请求会在 Redis 层被拦住——但必须配合数据库乐观锁兜底,否则库存和订单会不一致。
为什么不能只用 redis()->get() + redis()->decrBy() 两步走
因为 GET 和 DECRBY 是两次独立网络调用,中间存在竞态窗口:A 请求刚读到 stock=1,B 请求也读到 stock=1,两者都判断“够”,然后都执行 DECRBY,最终变成 -1。
- 错误写法:
$stock = redis()->get('stock:1001'); if ($stock >= $num) { redis()->decrBy('stock:1001', $num); } - 正确做法:用 Lua 脚本把“读值→比大小→扣减”封装成原子操作,或直接依赖
decrBy()返回值做拦截(它本身是原子的,但需注意返回逻辑) -
decrBy()返回的是扣减后的剩余值,不是是否成功。若返回-5,说明已超卖,必须拒绝,不能只看是否为整数
TP8.0 中 redis()->decrBy() 防超卖的标准流程
Redis 层只负责“预扣减+快速拦截”,不是最终落库。它的返回值就是第一道防线。
- 调用
$remain = redis()->decrBy('stock:1001', $buyCount) - 若
$remain ,立即 <code>return json(['code' => 400, 'msg' => '库存不足']),不再进 DB - 若
$remain >= 0,才继续查数据库当前version或stock值,发起带条件的 UPDATE - UPDATE 必须包含
WHERE stock >= {$buyCount} AND version = {$oldVersion},且检查Db::raw('rows_affected')是否为 1
数据库更新失败后,Redis 库存怎么回滚
DB 更新失败时,Redis 已经扣掉了,这部分库存不会自动回来——必须手动补回,否则导致“少卖”。
- 在
catch块里立刻执行redis()->incrBy('stock:1001', $buyCount) - 不要依赖事务自动回滚:Redis 操作不在 MySQL 事务范围内,
Db::transaction()对它无效 - 务必放在
catch或finally中,哪怕只是记录日志也要确保执行;漏掉一次,库存就永久少掉 - 如果用了 Lua 脚本做预扣,回滚逻辑也要写进脚本里(比如返回失败时自动 INCRBY),否则仍要靠应用层补偿
什么时候该上 Lua 脚本而不是裸用 decrBy()
当你的 Redis 网络延迟不稳定,或对一致性要求极高(比如金融级秒杀),就必须用 Lua 把校验和扣减锁死在一个原子上下文里。
- 裸
decrBy()依赖返回值判断,但网络超时、客户端崩溃会导致你收不到返回,无法确认是否已扣 - Lua 示例中
if stock ,返回 0/1/-1 语义明确,无歧义 - TP8.0 调用方式:
$result = redis()->eval($luaScript, ['stock:1001'], [$buyCount]);,注意参数顺序:KEYS 在前,ARGV 在后 - 别忘了给 Lua 脚本加注释并存为常量,避免每次拼字符串,也方便统一灰度和替换
最易被忽略的一点:Redis 库存初始化必须和数据库强一致,上线前跑一次全量同步脚本,之后所有扣减只走 Redis+DB 双写闭环;否则冷启动时 Redis 为空,一上来就全放行。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











