laravel缓存锁必须使用redis驱动才可靠,database或file驱动因缺乏原子性在高并发下失效;cache::lock()需配合原子命令,键名应含业务上下文与环境前缀,ttl须大于95%业务耗时,且需结合数据库行锁保障最终一致性。

缓存锁不是万能开关,用错地方反而会放大并发问题。Laravel 的 Cache::lock() 只在 Redis 驱动下真正可靠,database 或 file 驱动几乎无法保证原子性,高并发下会失效。
Cache::lock() 必须搭配 Redis 驱动
底层依赖原子命令(如 Redis 的 SET + NX + PX),database 驱动靠 INSERT ... ON DUPLICATE KEY 模拟,但 MySQL 在 RR 隔离级别下可能被间隙锁阻塞;file 驱动靠 PHP 文件锁,进程间不共享、无超时控制,生产环境禁用。
-
CACHE_DRIVER=redis是硬性前提,确认config/cache.php中stores.redis.connection与config/database.php的 Redis 连接名一致 - 若使用 Laravel Octane,需确保 Redis 连接是
persistent模式,否则每次请求重建连接导致锁状态丢失 - 执行
php artisan tinker --execute="dd(Cache::lock('test', 1)->get())"返回true才算通路正常
非阻塞 vs 阻塞:get() 和 block() 的真实行为差异
->get() 是立即返回:成功则执行闭包,失败直接跳过,不等;->block(5) 是最多等待 5 秒,期间不断轮询,一旦锁释放立刻抢占——但轮询本身有开销,且若业务逻辑耗时超过锁 TTL,其他请求仍可能抢入。
- 适合短时临界区(如生成 token、更新统计计数):用
Cache::lock('key', 10)->get(fn() => {...}) - 适合必须串行化且可接受延迟的场景(如支付回调幂等处理):用
Cache::lock('pay_'.$id, 30)->block(10)->get(...) - 不要在
block()里调用外部 HTTP 请求或长事务,锁过期后你还在等,别人已进来了
锁键命名和生命周期管理的坑
锁键不是随便拼个字符串就行。Cache::lock() 不校验键合法性,但 Redis 对 key 长度、字符有限制;更关键的是,锁自动过期后若业务未完成,后续请求会认为“没人占着”,直接闯入。
- 键名建议含业务上下文+唯一标识+环境前缀,例如
stock:product_123:staging,避免不同环境混用同一把锁 - TTL 必须大于 95% 场景下的业务执行时间,可用日志埋点统计耗时再设值,别拍脑袋写 30 秒
- 闭包内禁止抛出未捕获异常:Laravel 会在异常时自动释放锁,但若你在 try-catch 里吞掉异常又没手动
del,锁就滞留到过期为止——这不是死锁,但会卡住后续请求
最易被忽略的一点:锁只管“谁先到”,不管“谁对”。如果多个请求都拿到锁后各自查 DB、改余额、再写回,照样超卖。真要保数据一致性,得配合数据库行锁(selectForUpdate())或乐观锁(version 字段),缓存锁只是第一道轻量防线。











