laravel本身不处理并发竞争,防的是多请求同时读写同一行数据导致的竞态;解决方案是数据库锁、原子操作(如db::update带条件更新)、队列异步化及redis缓存优化。

直接说结论:Laravel 本身不处理并发请求的“竞争”,它只是按 HTTP 请求顺序执行代码;真正要防的是多个请求同时读写同一行数据导致的竞态,比如库存超卖、余额错扣。解决方案不是让 Laravel “变快”,而是用数据库锁、原子操作、队列或缓存把冲突点切开。
用 DB::update() 做带条件的原子更新,别用 Model->save()
常见错误现象:用户抢购时,两个请求都查到 stock = 1,各自减 1 后都 save(),结果变成 -1。
这是因为 $product = Product::find($id); $product->stock--; $product->save(); 拆成了 SELECT + UPDATE 两步,中间有时间窗口,且 Eloquent 加载整行、触发访问器、事件等,锁持有时间更长。
正确做法是跳过模型层,用原生 SQL 一次完成判断和变更:
DB::update('UPDATE products SET stock = stock - 1, updated_at = NOW() WHERE id = ? AND stock >= 1', [$id]);
这个语句天然具备三个关键能力:
- 条件检查(
stock >= 1)和扣减在同一事务内原子执行 - 只影响 0 或 1 行,返回值可直接用于业务判断:
if (DB::affectingStatement(...) === 0) { // 库存不足 } - 不触发 Eloquent 的
updating/updated事件,避免意外副作用
需要强一致性时,用 SELECT FOR UPDATE 显式加锁
适用场景:必须先读再改、且后续逻辑复杂(比如要校验多张表状态、调用外部服务、生成唯一单号),不能靠单条 SQL 完成。
关键点在于锁的时机和范围:
- 必须在事务最开始就执行
SELECT ... FOR UPDATE,不能先查后进事务 - WHERE 条件尽量用主键或带索引的字段,避免全表扫描锁住无关行
- 不要用
Product::lockForUpdate()->find($id),它可能因懒加载触发额外查询,延长锁持有时间 - 推荐写法:
DB::select('SELECT * FROM products WHERE id = ? FOR UPDATE', [$id])
注意:innodb_lock_wait_timeout 是 MySQL 层锁等待超时(默认 50 秒),而 PDO::ATTR_TIMEOUT 只控制连接建立超时,不是锁等待超时,别混淆。
高并发写入压力大?优先走队列,而不是死磕实时 DB 更新
当多个请求都要更新同一张表(比如记录用户行为日志、统计 PV/UV),直接写库会引发大量行锁争抢,甚至拖慢整个应用。
更务实的做法是把“写”异步化:
- Web 请求只做轻量校验和入队,例如:
ProcessUserAction::dispatch($userId, $action)->onQueue('logs'); - 用 Supervisor 管理多个
php artisan queue:work进程,并发消费(numprocs=4表示起 4 个 worker) - 选 Redis 驱动而非 database 驱动,后者在高并发下容易因
jobs表锁导致任务获取延迟 - 失败重试加
--tries=3,配合failed_jobs表人工兜底
这不是逃避问题,而是把“必须立刻落库”的假设打掉——多数业务场景中,“秒级最终一致”完全可接受,且稳定性高得多。
Redis 缓存能挡掉 70% 以上读请求,但要注意连接复用方式
很多人配了 Redis 缓存,却没意识到 PHP-FPM 下每次请求仍新建 TCP 连接,高频缓存读反而加重 Redis 侧连接压力。
真实有效的优化只有两条路:
- 用 PhpRedis 扩展 +
'persistent' => true配置,在 FPM 子进程内复用连接(注意:仅限同个 FPM worker 生命周期内) - 切换到 Swoole 常驻进程环境后,才谈得上真正的协程连接池,每个 Worker 持有独立连接实例,无跨请求握手开销
别信“Laravel 数据库连接池”这类伪概念——PHP-FPM 天然无状态,所谓“池”只是连接缓存,MySQL 侧连接数该涨还是涨。真要压测扛住 5000+ QPS,得从运行模型(Swoole/RoadRunner)和架构分层(CDN → 缓存 → 队列 → DB)一起动刀,单靠改 Laravel 配置没用。











