订单系统需实现幂等校验、原子化库存扣减、异步队列解耦及兜底核对机制:用Redis指纹防重提交;原生SQL原子扣库存;Web层仅入队,队列执行核心逻辑;pending_orders表+定时任务兜底核对。

订单入口必须做幂等校验,不能只靠前端防重
用户点一次“提交订单”,前端可能发多次请求(网络抖动、双击、F5 刷新),后端若不拦截,就会生成多条重复订单。Laravel 默认不带全局幂等控制,OrderController@submit 里不能只依赖 session 或 token 做一次校验就放行。
实操建议:
- 用 Redis 实现「请求指纹」:对用户 ID + 商品 ID + 时间戳(如精确到分钟)拼接后取 hash,
SETNX order_fingerprint:{$hash} 1 EX 60,失败直接返回「请勿重复提交」 - 避免用时间戳本身作 key,防止同一秒内多个请求绕过;也别用全量参数拼接,太长且易被构造碰撞
- 这个校验要放在所有业务逻辑之前,包括权限检查、库存预占——否则风控或锁库存失败后,指纹还占着,用户反而无法重试
- 注意 Redis 连接超时或故障时的 fallback:可降级为内存缓存(仅限单机开发环境)或直接允许一次重试,但需记录告警日志
库存扣减必须原子化,别在事务里 SELECT FOR UPDATE 后再 UPDATE
常见错误是写成:$product = Product::lockForUpdate()->find($id); $product->stock--; $product->save();。这在高并发下会卡死,因为 lockForUpdate() 拿到锁后,Eloquent 还要走模型事件、访问器、批量赋值过滤等流程,锁持有时间远超必要——MySQL 的 innodb_lock_wait_timeout 默认 50 秒,等不及就报 SQLSTATE[HY000]: General error: 1205 Deadlock found。
实操建议:
- 改用原生原子 SQL:
DB::update('UPDATE products SET stock = stock - 1, updated_at = NOW() WHERE id = ? AND stock >= ?', [$id, $quantity]) - 检查影响行数:
$affected = DB::affectingStatement(...),等于 0 表示库存不足,直接拒绝,不进后续流程 - 如果业务需要扣减后触发通知或日志,用队列异步补全,不要塞在同一个事务里
- 禁止在扣减 SQL 中加复杂 WHERE 条件(比如联合状态字段),会拖慢索引效率;确保
products.id和products.stock上有独立索引
订单创建与状态更新必须解耦,Web 请求只负责入队
用户点击下单后,如果在 HTTP 请求中同步完成「生成订单号→扣库存→发消息→更新账户→推结算」这一整条链路,任意一环超时(比如 RabbitMQ 暂不可用、第三方支付回调延迟),整个接口就挂了,还会导致重复请求不断重试,雪球越滚越大。
实操建议:
- Web 层只做三件事:幂等校验、参数验证、调用
PlaceOrderJob::dispatch($validatedData)->onQueue('orders') - 队列任务里才执行核心动作,且每个动作尽量短:扣库存 → 写订单主表 → 推送风控服务 → 记录操作日志,每步失败都单独处理,不 throw 全局异常
- 队列驱动必须用
redis,别用database:后者依赖 MySQL 行锁争抢,1000 QPS 下jobs表会成为瓶颈;Redis 的 LPUSH + BRPOP 是真正的无锁高性能队列 - 生产环境必须配 Supervisor 管理 worker,并设
numprocs=4起 4 个并发进程;--timeout=90防止长任务被误杀,--max-jobs=500防内存泄漏
成交结果不能只靠队列消费,得有兜底核对机制
哪怕用了 Redis 队列 + 多 worker,仍可能因网络分区、worker 进程崩溃、RabbitMQ 消息丢失等问题,导致某些订单“已入队但未成交”。用户看到“提交成功”,实际没生成订单,这种体验比失败更糟。
实操建议:
- 订单入队时,同时写一条
pending_orders表记录,含订单号、用户 ID、创建时间、状态(pending)、重试次数,默认 5 分钟过期 - 用 Laravel 的
scheduler每分钟跑一次php artisan check:pending-orders,查出超时未更新的记录,重新投递或标记为失败并通知运营 - 用户端查询订单时,优先查
orders表,查不到再查pending_orders并返回「处理中,请稍候」,避免直接 404 - 这个核对任务本身也要幂等,防止定时任务重跑造成重复推送
订单系统最复杂的不是怎么写代码,而是怎么定义“成功”——是用户看到提示就算,还是数据库落库才算,还是结算服务确认才算。不同环节的“成功”标准不一致,才是并发问题反复出现的根因。


















