laravel锁分三层:数据库行级锁(selectforupdate)、缓存分布式锁(cache::lock)、队列任务级控制(queue:work)。选错层易致无效、死锁或超卖。

直接说结论:Laravel 的锁不是“一种东西”,而是分三层——数据库行级锁(selectForUpdate())、缓存分布式锁(Cache::lock())、队列任务级并发控制(queue:work 多进程)。选错层,轻则无效,重则死锁或超卖。
什么时候必须用 selectForUpdate()?
仅当你要「先读、再做复杂判断、最后写」,且该判断无法下推到 SQL WHERE 条件里。比如:检查用户状态 + 余额 + 是否满足活动规则 + 计算优惠后金额,再扣款。
- 必须包裹在
DB::transaction()中,否则锁立刻释放;脱离事务的selectForUpdate()在 Laravel 里基本没用 - WHERE 条件必须命中索引,否则可能升级为表锁或间隙锁——例如
where('email', 'x@example.com')没给 email 加索引,就危险 - MySQL 默认 RR 隔离级别下,
selectForUpdate()锁的是「临键锁(Next-Key Lock)」,会锁住查询范围内的间隙,哪怕那行还不存在(防幻读),这容易导致不相关请求也被阻塞 - 多个请求同时锁多行时,若加锁顺序不一致(A 先锁 id=1 再锁 id=2,B 反过来),大概率触发
Deadlock found when trying to get lock
Cache::lock() 适合什么场景?
适用于「非数据库核心路径」的同步控制,比如生成临时凭证、刷新某份聚合缓存、防止重复提交表单、避免同一用户短时间内重复触发支付回调处理。
- 底层依赖 Redis(推荐)或 Memcached,
database驱动性能差、不建议用于高并发锁 -
Cache::lock('key', $ttl)->get()是非阻塞的:失败直接返回 false,不等;想等就加->block(5),最多等 5 秒 - 锁自动过期,但业务逻辑里不能有长耗时操作(如 curl 外部接口),否则锁过期了你还在跑,失去意义
- 别拿它替代数据库一致性校验——它只管「谁先拿到执行权」,不管「执行结果是否合法」
为什么原子 UPDATE 比 selectForUpdate() 更常用?
绝大多数资金/库存类操作,其实根本不需要先读——只要 SQL 能把「条件校验 + 修改」合并成一条语句,就该优先用它。
- 示例:
DB::update('UPDATE users SET balance = balance - ? WHERE id = ? AND balance >= ?', [$amount, $id, $amount]),返回值是影响行数,0 就代表余额不足或已被扣完 - 锁时间极短:只在 UPDATE 执行瞬间持有行锁,不像
selectForUpdate()从 SELECT 开始就锁着,中间 PHP 逻辑、日志、异常处理全算在锁期内 - 无死锁风险:单条语句、单次加锁、主键精确命中,不会产生间隙锁干扰其他查询
- 但限制明显:只能做简单数值运算(
increment()、decrement())或条件更新;没法做 if-else 分支、调外部 API、查关联表后再决定怎么改
队列任务并发和锁的关系
很多人以为「开了多个 queue:work 就要自己加锁」,其实反了:队列本身是天然的并发协调器,加锁反而画蛇添足。
- 每个
queue:work进程独立争抢任务,Redis 或 database 驱动保证「一个任务只被一个 worker 拿走」,这是原子出队,不是靠你写锁实现的 - 只有当你明确需要「同一类任务(如处理用户 ID=123 的订单)不能并行执行」时,才在
handle()开头加Cache::lock("order_{$this->orderId}") - 别在队列任务里用
selectForUpdate()做常规扣减——既然已进队列,说明请求已削峰,用原子 UPDATE 更稳更快 - Supervisor 配置里的
numprocs=4是并发 worker 数,不是「能同时处理多少个相同 ID 的任务」,后者得靠业务锁区分
最易被忽略的一点:锁的有效性永远取决于它的作用域是否覆盖全部竞态路径。比如用 Cache::lock() 防止重复下单,但没在数据库层面加唯一索引约束(如 order_no),一旦缓存故障或锁失效,照样可能写入两条重复订单。











