必须确认缓存驱动支持锁,仅 Redis 或 Memcached 驱动实现 LockContract;file、array 等驱动调用 Cache::lock() 会抛出 BadMethodCallException,需通过 cache()->store()->supportsLocks() 验证返回 true 后方可使用。

用 Cache::lock() 前先确认缓存驱动是否支持
不是所有缓存驱动都实现 LockContract,比如 file、array 驱动调用 Cache::lock() 会直接抛出 BadMethodCallException。必须确保 config('cache.default') 指向 redis 或 memcached。
检查方法:运行 php artisan tinker 后执行 cache()->store()->supportsLocks(),返回 true 才安全。
-
redis驱动底层用SET key value EX seconds NX,原子性强 -
memcached依赖add()+delete(),不支持原子续期,超时后可能误释放 - 若用自建 Redis 集群,需确认客户端支持
EVAL(解锁 Lua 脚本必需)
lockForUpdate() 必须和事务一起用,且不能链在模型方法后
Order::find(123)->lockForUpdate() 是无效的——find() 已执行完查询,锁早就释放了。真正起作用的是在查询构建阶段加锁:
DB::transaction(function () {
$order = Order::where('id', 123)
->where('status', 'pending')
->lockForUpdate()
->firstOrFail();
$order->status = 'processing';
$order->save();
});
- WHERE 条件必须能命中索引,否则会升级为表锁,拖垮并发性能
- 事务内不要做 HTTP 请求、文件写入等长耗时操作,锁持有时间越短越好
- MySQL 默认隔离级别
REPEATABLE READ下,lockForUpdate只锁住已匹配的行,但幻读场景仍需配合SELECT ... FOR UPDATE显式范围锁定
Redis 锁的 key 必须带业务粒度,且值要唯一可验
用固定字符串如 'order_lock' 当 key,等于把全站订单串行化。正确做法是按资源 ID 构建 key,并用随机 token 标识持有者:
- key 示例:
'lock:order_123'、'lock:stock_sku_456' - 加锁时设值为
uniqid() . '_' . getmypid(),避免多进程误删 - 解锁必须用 Lua 脚本比对 value,不能直接
Redis::del('lock:order_123') - 过期时间(TTL)建议设为业务逻辑最坏执行时间 × 1.5,比如下单逻辑通常 2s,TTL 设 3~5s 较稳妥
MongoDB 分布式锁要注意 TTL 索引是否已建
laravel-mongodb 的 MongoLock 依赖 expires_at 字段上的 TTL 索引自动清理死锁。但这个索引不会自动创建,必须手动触发:
php artisan mongodb:ensure-ttl-index
或在代码中显式调用 $lock->createTTLIndex()。没建索引时,过期锁不会被自动删除,长期运行后集合膨胀,查询变慢,甚至导致新锁获取失败。
另外,MongoDB 的 findOneAndUpdate 原子更新虽强,但要注意:$cond 表达式里的时间比较必须用 UTC 时间戳,Laravel 默认时区偏差会导致锁提前失效或永久卡住。


















