结论:laravel 5.5 中必须用 cache::lock() + redis 驱动实现分布式锁,db 锁(transaction/lockforupdate/get_lock)无法跨进程跨机器协调,并发场景下必然导致重复执行或死锁。

直接说结论:Laravel 5.5 中必须用 Cache::lock() + Redis 驱动,不能依赖数据库锁或 GET_LOCK();否则在队列、多服务器场景下必然出现重复执行或死锁。
为什么 DB::transaction() + lockForUpdate 不够用
它只管单机单连接内的行锁,对跨进程、跨机器的并发完全无效。比如一个队列任务和一个 HTTP 请求同时处理同一订单,lockForUpdate 各自锁各自的事务,互不感知——这是最常被忽略的盲区。
- MySQL 的
GET_LOCK()是会话级锁,队列 worker 复用 PDO 连接时锁状态混乱,服务重启后锁全丢 -
DB::transaction()默认不阻塞等待,lockForUpdate查不到行就直接返回空,不会等其他事务释放 - Redis 是唯一能统一协调所有 PHP 进程(包括 artisan 命令、队列、Web 请求)的锁媒介
Cache::lock() 必须搭配 Redis 驱动启用
5.5 版本中 Cache::lock() 仅在 redis 或 memcached 驱动下可用,file 和 array 会静默失败或抛出 BadMethodCallException。
- 确认
config/cache.php中'default' => 'redis',且config/database.php的redis配置已生效 - 不要用
Redis::setnx()手写逻辑——5.5 的Cache::lock()底层已封装SET key value NX PX 10000原子操作,更可靠 - 锁 key 必须带业务标识,例如
'order_lock:123',而非固定字符串'order_lock',否则所有订单串行执行
block() 超时与自动释放的坑
block(5) 表示最多等 5 秒,但底层每 250ms 尝试一次,期间若网络抖动可能误判失败;而 get(callback) 方式会在 callback 执行完自动 release(),但前提是 callback 不抛异常——否则锁不会释放。
- 高延迟操作(如调用第三方 API)必须延长 ttl,例如
Cache::lock('key', 60)->block(10),避免锁提前过期 - 绝不要写
if ($lock->get()) { ... $lock->release(); }—— 若中间发生 Exception,release()不会被调用,锁永久残留 - 正确姿势是
Cache::lock('key', 30)->block(5, function () { /* 业务 */ });,框架保证 callback 执行后必 release - 如果需手动控制释放时机(如跨进程传递锁),用
$lock->owner()记录 token,再通过Cache::restoreLock('key', $token)->release()
锁粒度与唯一索引的配合点
分布式锁解决的是“谁先拿到谁执行”,但不保证业务逻辑本身无竞态。例如库存扣减,光锁住 'stock_lock:sku_456' 不够——若两个请求都拿到锁,仍可能同时读到库存为 1,各自减 1 写回 0。
- 简单场景优先用原子 SQL:
DB::update('UPDATE products SET stock = stock - 1 WHERE sku = ? AND stock >= 1', [$sku]),影响行为 0 则说明库存不足 - 复杂逻辑(如校验用户资格、生成多条关联记录)才用锁 + 事务组合,且事务内第一句必须是
lockForUpdate()查询 - 所有锁 key 涉及的字段(如
order_no、sku)必须建唯一索引,否则幻读风险无法规避
真正难的不是加锁,而是判断哪里该加、加多大粒度、以及锁住之后下一步是否还存在竞态——这些没法靠框架自动解决,得贴着业务逻辑一层层压测验证。











