Laravel本身不解决并发竞争,需用原子SQL、行锁、队列或补偿机制隔离写操作;DB::update带条件更新替代Model->save(),SELECT FOR UPDATE须在事务首句执行且WHERE走索引,高并发写入优先走队列,跨服务用本地消息表+幂等设计实现最终一致性。

直接说结论:Laravel 本身不解决并发竞争,它只是顺序执行请求;真正要防的是多个请求同时读写同一行数据引发的竞态,比如库存扣成负数、订单重复创建。核心思路不是“让 Laravel 更快”,而是把冲突点切开——用原子 SQL、行锁、队列或补偿机制隔离写操作。
DB::update 带条件更新必须替代 Model->save()
常见错误现象:$product = Product::find($id); $product->stock--; $product->save(); 看似简单,实则拆成 SELECT + UPDATE 两步,中间有时间窗口。两个请求都查到 stock = 1,各自减 1 后都写回,结果变成 -1。
- 改用原生原子语句:
DB::update('UPDATE products SET stock = stock - 1 WHERE id = ? AND stock >= 1', [$id]); - 返回值可直接判断是否成功:
if (DB::affectingStatement() === 0) { // 库存不足 } - 不触发 Eloquent 的
saving/updated事件,避免副作用 - WHERE 条件必须含约束(如
stock >= 1),否则起不到防护作用
SELECT FOR UPDATE 要在事务开头立即执行
适用场景:不能靠单条 SQL 完成的复杂逻辑,比如先查库存、再校验用户等级、再调外部支付接口、最后扣减并生成单号。
- 必须在
DB::transaction()开始后**第一句**就执行:DB::select('SELECT * FROM products WHERE id = ? FOR UPDATE', [$id]); - 别用
Product::lockForUpdate()->find($id),它可能触发懒加载,延长锁持有时间 - WHERE 必须走主键或带索引字段,否则会升级为表锁,拖垮整个表
- 注意:
innodb_lock_wait_timeout控制锁等待超时(默认 50 秒),PDO::ATTR_TIMEOUT只管连接建立,不是锁超时
高并发写入优先走队列,别硬扛实时 DB 更新
当多个请求都要往同一张表写(比如记录用户行为日志、统计 PV/UV),直接同步写库会引发大量行锁争抢,甚至阻塞其他业务。
- Web 请求只做轻量校验和入队:
ProcessUserAction::dispatch($userId, $action)->onQueue('logs'); - 队列任务里再批量写、聚合写或异步落库,降低 DB 压力
- 对一致性要求不高的场景(如统计类),可接受秒级延迟,队列比事务更 scalable
- 队列失败需有重试 + 死信处理,不能丢数据
跨服务操作必须用补偿机制,单库事务无效
Laravel 拆成微服务后,订单服务扣款、库存服务扣库存、通知服务发短信——这三个操作无法放进一个数据库事务。
- 本地消息表是落地最稳的方案:在订单事务内插入一条
status = pending的消息记录,事务提交后再由队列异步推送下游 - 下游服务收到消息后,先查自己是否已处理该
message_id(幂等设计),再执行业务 - TCC 或 Saga 模式适合强一致性要求,但开发成本高,需每个服务提供
try/confirm/cancel接口 - 所有跨服务方案都必须有人工干预兜底能力,比如定时扫描
pending消息并告警
最容易被忽略的一点:事务不是银弹。死锁、锁等待、长事务都会拖慢响应;而缓存与 DB 不一致、空值穿透、版本错乱这些“软性”问题,往往比事务失败更难排查。得把原子 SQL、行锁、队列、补偿、缓存校验这几层手段按场景组合使用,而不是指望某一层包打天下。


















