首页/新闻资讯/正文详情

老项目库存超卖陷阱:一招修复并发漏洞

发布时间:2026/11/6 5:25:58 来源:云帆数科 栏目:资讯中心
老项目库存超卖陷阱:一招修复并发漏洞
上周帮朋友排查一个线上问题商城后台显示库存还剩 3 件但同一时间有 5 个订单都支付成功了。翻日志翻了两个小时最后定位到一段看起来毫无问题的代码——一个老 Laravel 项目里的库存扣减逻辑。说实话这种坑我见过不止一次了而且踩的人远比想象中多。这篇文章就把这个静默陷阱拆开讲讲包括它为什么会出现、怎么复现、以及几种不同场景下的修法。如果你手上也有跑了两三年的 Laravel 项目建议对照着看一眼。一、问题现场一段看起来没问题的代码先看最常见的写法很多老项目里都是这个套路public function deduct($productId, $quantity) { $product Product::find($productId); if ($product-stock $quantity) { throw new \Exception(库存不足); } $product-stock $product-stock - $quantity; $product-save(); return true; }单线程跑测试一点毛病没有。但线上是并发环境两个请求几乎同时进来都执行了find()都读到stock 3都判断 3 1 通过然后各自save()写回 2。结果就是卖了 2 件库存只减了 1 件。更麻烦的是这段代码不会报错不会抛异常日志里干干净净。等你发现的时候往往已经是财务对账对不上了。二、为什么说它是静默的我总结下来有三个原因让这个坑特别难被发现测试环境压不出来本地或者测试环境并发量低两个请求撞上的概率极小跑一百次都是对的。没有异常信号Eloquent 的save()是后写覆盖它不关心你读到的值是不是最新的所以不会报错。问题滞后暴露库存对不上通常要等到盘点或者用户投诉才被发现中间可能已经过去好几天。换句话说这是一个正确性问题而不是可用性问题。系统一直好好的只是数据悄悄错了。三、复现一下眼见为实想验证其实很简单用 Laravel 自带的任务队列或者直接开多进程压一下就行。下面这个脚本可以快速复现// routes/web.php 里临时加一个测试路由 Route::get(/test-stock, function () { $product Product::find(1); $product-stock 100; $product-save(); $barrier new \GuzzleHttp\Promise\Utils(); // 仅示意实际可用 curl_multi // 用 20 个并发请求同时扣减 $urls array_fill(0, 20, url(/deduct/1)); $mh curl_multi_init(); $handles []; foreach ($urls as $url) { $ch curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_multi_add_handle($mh, $ch); $handles[] $ch; } do { curl_multi_exec($mh, $running); curl_multi_select($mh); } while ($running 0); return Product::find(1)-stock; // 大概率不是 80 });跑完你会发现库存减掉的数量明显少于 20。这就是竞态在真实环境里的样子。四、几种修法按项目情况选方案一数据库原子操作推荐最省事也最可靠的做法是把读-判断-写合并成一条 SQL交给数据库保证原子性public function deduct($productId, $quantity) { $affected DB::table(products) -where(id, $productId) -where(stock, , $quantity) -decrement(stock, $quantity); if ($affected 0) { throw new \Exception(库存不足); } return true; }注意这里用decrement而不是先查再改条件写在where里数据库会在行锁的保护下完成判断和更新。返回的影响行数为 0 就说明库存不够逻辑非常干净。方案二悲观锁如果扣减逻辑比较复杂比如还要写流水、算积分那可以用lockForUpdate把整行锁住DB::transaction(function () use ($productId, $quantity) { $product Product::where(id, $productId)-lockForUpdate()-first(); if ($product-stock $quantity) { throw new \Exception(库存不足); } $product-stock - $quantity; $product-save(); // 这里可以继续写库存流水 StockLog::create([ product_id $productId, change -$quantity, ]); });要注意lockForUpdate必须放在事务里才有效而且锁的粒度别太大不然高并发下容易排队。方案三乐观锁如果冲突概率不高乐观锁的性能更好。给表加一个version字段$affected DB::table(products) -where(id, $productId) -where(version, $version) -where(stock, , $quantity) -update([ stock DB::raw(stock - . (int) $quantity), version $version 1, ]); if ($affected 0) { // 版本冲突或库存不足重试或直接失败 }这种写法适合秒杀之外的普通商品冲突少重试成本低。五、几个容易忽略的细节修完主逻辑还有几个地方值得顺手检查一下缓存里的库存很多项目会在 Redis 里存一份库存做前置拦截记得和数据库的扣减保持一致别只改一边。队列任务重复消费如果扣减是异步的要保证幂等否则重试一次就多扣一次。事务隔离级别MySQL 默认是可重复读某些场景下select读到的可能是快照数据配合锁使用时要留意。超卖兜底即使逻辑写对了也建议加一个定时任务做库存校准作为最后一道防线。六、写在最后这类问题的本质是把并发场景当成了串行场景来写代码。Laravel 的 Eloquent 用起来太顺手了find加save两行就搞定反而容易让人忘记底下还有并发这回事。如果你正在维护一个老项目不妨全局搜一下-stock、-save()这类组合看看有没有类似的写法。改动量不大但能省掉后面很多对账的麻烦。最后提醒一句修完之后一定要用并发脚本压一遍别只跑单元测试。竞态这种东西只有真的撞上了才算验证过。

相关推荐

LeetCode 169. 多数元素:从排序到摩尔投票的三种解法
LeetCode 169. 多数元素:从排序到摩尔投票的三种解法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 5:25:58

空圈容错落进Python:用架构约束把R4焊死在CI里
空圈容错落进Python:用架构约束把R4焊死在CI里

定位:这不是一篇“Python架构最佳实践”,是用空圈容错理论的 R4(跨层跳步)和 R2(假异构)去诊断 Python 项目里“依赖方向悄悄焊反”的问题,并给出一个可落地的切入口——import-linter 的 layer… · 2026/11/6 5:24:58

如何在 Windows 11/10 中查找最近关闭的应用[内置 + 免费工具]
如何在 Windows 11/10 中查找最近关闭的应用[内置 + 免费工具]

快速答案 可以,你可以在 Windows 11 或 10 里查看最近关闭的应用,但也不完全是那么回事。不过,某种程度上你可以借可靠性监视器、事件查看器,或 NirSoft 的 LastActivityView 这类第三方工具做到。Windows 本身并不提供直接的“最近关闭的应用”历史记录;但这些方法能让你… · 2026/11/6 5:24:58

GSYVideoPlayer:Android 视频播放从接入到切换内核的实践笔记
GSYVideoPlayer:Android 视频播放从接入到切换内核的实践笔记

GSYVideoPlayer:Android 视频播放从接入到切换内核的实践笔记 【免费下载链接】GSYVideoPlayer Video players (IJKplayer, ExoPlayer, MediaPlayer), HTTPS, 16k page size, danmaku (bullet chat) support, external subtitles, support for filters, watermarks,… · 2026/11/6 6:15:10

Leon Sans 快速入门:3 步把动态字重和文字动画画进网页
Leon Sans 快速入门:3 步把动态字重和文字动画画进网页

Leon Sans 快速入门:3 步把动态字重和文字动画画进网页 【免费下载链接】leonsans Leon Sans is a geometric sans-serif typeface made with code in 2019 by Jongmin Kim. 项目地址: https://gitcode.com/gh_mirrors/le/leonsans Leon Sans 是一个用 JavaS… · 2026/11/6 6:15:10

光伏电站、风机点位空间分布数据集整理
光伏电站、风机点位空间分布数据集整理

光伏电站/光伏面板 1. 基于GEE和Sentinel-2用随机森林分类算法生成的全国2020年10m分辨率光伏电站数据 A 10-m national-scale map of ground-mounted photovoltaic power stations in China of 2020 数据分省存放,因为数据是分辨率较高的栅格数据类型&#xff0… · 2026/11/6 6:15:10

Mac触控板在Windows上找回原生手感:mac-precision-touchpad 完整配置攻略
Mac触控板在Windows上找回原生手感:mac-precision-touchpad 完整配置攻略

Mac触控板在Windows上找回原生手感:mac-precision-touchpad 完整配置攻略 【免费下载链接】mac-precision-touchpad Windows Precision Touchpad Driver Implementation for Apple MacBook / Magic Trackpad 项目地址: https://gitcode.com/gh_mirrors/ma/mac-pre… · 2026/11/6 6:15:10

终极指南:ChatGPT查询工程师手册解锁AI交互新维度 [特殊字符]
终极指南:ChatGPT查询工程师手册解锁AI交互新维度 [特殊字符]

终极指南:ChatGPT查询工程师手册解锁AI交互新维度 🚀 在AI技术飞速发展的今天,ChatGPT查询工程师手册成为了每个开发者必备的AI交互宝典。这份手册专为ChatGPT提问工程师设计,帮助大家在与ChatGPT合作编程中大幅提升工作效率。✨ … · 2026/11/6 6:15:10

智能车竞赛电路设计全复盘:从BUCK电源到电机驱动的实战经验
智能车竞赛电路设计全复盘:从BUCK电源到电机驱动的实战经验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 6:14:10

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07

三菱PLC数据采集到数据库的时序设计:Python+MC协议实战
三菱PLC数据采集到数据库的时序设计:Python+MC协议实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 1:02:11

ASIC主模式RTL设计:状态机与三态驱动的流片级实现
ASIC主模式RTL设计:状态机与三态驱动的流片级实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 1:02:11

STM32嵌入式C++开发:GDB调试与Renode仿真实战指南
STM32嵌入式C++开发:GDB调试与Renode仿真实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 1:02:11

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码