Laravel 无锁队列实为驱动层原子操作实现:Redis 用 BRPOP/ZSET 原子出队,Database 依赖行锁,PHP 层不加锁是因锁在底层;优化关键在选对驱动、绕过 Eloquent、避免事务膨胀。

无锁队列在 Laravel 里并不存在——Laravel 的队列机制本身依赖驱动层的原子操作(如 Redis 的 BRPOP、Database 的行锁),不是靠 PHP 层“不加锁”实现高并发,而是靠驱动层的**无竞争出队设计**来规避应用层锁争用。真正能提升并发效率的,是选对驱动、绕过 Eloquent、避免事务膨胀,而不是幻想“无锁”。
为什么不能指望 PHP 层实现无锁队列
Laravel 的 queue:work 进程之间完全独立,彼此不通信;所谓“无锁”,其实是把锁下推到 Redis 或数据库底层:Redis 用单线程 + 原子命令保证 LPOP/ZREM 不冲突,MySQL 用 SELECT ... FOR UPDATE 或 UPDATE ... WHERE 条件更新隐式加锁。PHP 层不做同步控制,不是因为“无锁”,而是因为“锁不在它手上”。强行在代码里用 file_put_contents() 或 apcu_store() 模拟队列,会直接崩在并发写入或进程隔离上。
Redis 驱动才是事实上的“无竞争”队列基础
用 Redis 作队列驱动时,BRPOP 是关键:它阻塞等待、原子出队、失败不丢任务。相比 database 驱动需轮询 jobs 表并尝试 UPDATE ... WHERE reserved_at IS NULL,Redis 天然避免了表锁、间隙锁、事务回滚干扰。
-
QUEUE_CONNECTION=redis必须与queue:work命令中的--queue参数一致,否则 worker 根本连不到队列 - 延迟任务走 Redis 的
ZSET,score 是时间戳,worker 轮询时用ZRANGEBYSCORE+ZREMRANGEBYSCORE原子迁移,精度秒级,无竞态 - 别用
redis::lpush+redis::rpop手动模拟队列——缺少失败重试、超时标记、重试计数等 queue 组件内置保障
绕过 Eloquent 和事务,用原子 SQL 实现“逻辑无锁”
很多场景所谓“高并发瓶颈”,其实卡在任务处理逻辑里——比如秒杀扣库存时,用 Product::find()->decrement() 触发两次查询+一次 update+一堆事件,锁住整行几十毫秒。这时真正的优化不是改队列,而是让单个任务执行得更快、更轻、更原子:
- 用
DB::update('UPDATE products SET stock = stock - 1 WHERE id = ? AND stock >= 1', [$id])替代模型操作,成功返回 1 行影响,失败返回 0,无需事务包裹 - 避免在
handle()里调用DB::transaction(),除非真要跨多表强一致性;多数扣库存、发通知场景,单条带条件 update 就够了 - 如果必须读后再写(比如要校验用户等级+生成订单号+扣库存),那就用
DB::select('SELECT * FROM products WHERE id = ? FOR UPDATE', [$id])显式加锁,并立刻完成所有 DB 操作,不要在锁内做 HTTP 请求或文件写入
容易被忽略的致命点:failed_jobs 表和重试策略
并发越高,失败任务越容易堆积。database 驱动下,failed_jobs 表若没加索引(比如 failed_at 字段),queue:retry 或 Horizon 清理时会全表扫描;Redis 驱动虽无此问题,但若配置 --tries=1 + --delay=0,失败任务会立即重入队列,形成雪崩。
务必检查:你的 config/queue.php 中 redis 驱动的 retry_after(默认 90 秒)是否大于最长任务耗时;database 驱动的 retry_after 必须小于 MySQL 的 innodb_lock_wait_timeout,否则重试时锁还没释放就又撞上了。


















