本文目录导读:

是的,Laravel 在处理防重入(即防止重复提交或重复执行)时,强烈推荐使用分布式锁,特别是当你的应用运行在多个服务器(负载均衡)或使用队列多进程消费时。
单纯的数据库唯一索引或Cache::add()(原子检查)只能解决部分场景,而基于 Redis 或数据库的分布式锁才是真正应对并发、跨进程、跨机器的重入问题的标准方案。
下面详细说明为什么需要以及如何在 Laravel 中实现。
为什么需要分布式锁?
- 跨进程/跨服务器:单机锁(如
synchronized、Mutex)只能在同一个 PHP 进程内生效,如果启用了多个php-fpm进程或有多台服务器,无法阻止并发请求。 - 队列消费:Laravel 队列的
worker通常启动多个进程,如果队列任务未正确处理幂等性,同一任务可能被多个 worker 同时消费导致数据重复。 - 接口防重:用户快速点击“提交订单”,两个请求到达不同服务器,数据库事务中检查库存时都可能通过,导致超卖。
Laravel 原生支持:Cache::lock()
从 Laravel 5.6 开始,框架内置了 原子锁(Atomic Locks)功能,只要配置了 redis 或 memcached 驱动(推荐 Redis),即可获得分布式锁能力。
基础用法
use Illuminate\Support\Facades\Cache;
$lock = Cache::lock('order:create:user_'.$userId, 10); // 锁名称,超时10秒
if ($lock->get()) {
// 获取到锁,执行关键业务
try {
// 创建订单(防重入的关键代码)
} finally {
// 必须释放锁
$lock->release();
}
} else {
// 未获取到锁,表示已有请求在处理
return response('正在处理中,请勿重复提交', 429);
}
更简洁的写法(推荐)
Cache::lock('order:create:user_'.$userId, 10)->block(5);
// block(5) 表示如果未获取到锁,最多等待 5 秒尝试获取
如果你不需要等待,只是想“有锁就处理,没锁就放弃”,用上面的条件判断即可。
队列场景:防止任务重入
如果你的任务本身就有重入风险(如调用第三方API超时后重试),使用 Cache::lock 来确保同一任务只执行一次。
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Cache;
class ProcessPayment implements ShouldQueue
{
public $tries = 3; // 允许重试
public function handle()
{
$lockKey = 'payment:lock:'.$this->orderId;
// 获取一个持续到任务运行结束的锁(加超时防止死锁)
$lock = Cache::lock($lockKey, 30);
if ($lock->get()) {
try {
// 执行支付逻辑
// 即使队列重试,其他进程也拿不到锁
} finally {
$lock->release();
}
} else {
// 如果已存在锁,可能是上一个 worker 正在处理
// 可以选择释放任务(避免重复执行)
$this->delete();
}
}
}
更高级的用法(避免阻塞)
block() 方法会阻塞当前进程直到获取到锁或超时,对于接口防重,通常不希望阻塞太久:
// 获取锁,最多等待3秒,如果3秒后还没拿到,抛出异常或返回失败
if ($lock = Cache::lock('foo', 10)) {
// 尝试立即获取(非阻塞)
if ($lock->get()) {
// ...
}
}
实现分布式锁的底层原理
Laravel Cache::lock() 基于 Redis 的 SET NX EX 原子命令(或类似机制),确保:
- 原子性:只有一个进程能成功设置 key。
- 超时自动释放:通过
EX(过期时间)防止进程崩溃导致死锁。 - 所有者识别:Laravel 会生成一个随机值(token)存储在锁中,释放时只有拥有该 token 的进程才能释放,避免误删其他进程的锁。
注意事项
-
锁超时时间:必须大于业务的预期执行时间,如果业务执行时间超过锁超时时间,锁会自动释放,其他请求可能进入,建议:
- 预估业务执行时间,设置超时时间为其 2-3 倍。
- 如果业务不可预估(如调用外部慢 API),考虑使用 续期机制(Laravel 不内置,需自己实现或使用 Laravel Octane / 其他库)。
-
使用同一缓存驱动:所有服务器必须配置同一个 Redis 实例(或集群),才能共享锁状态。
-
幂等性设计 + 分布式锁:分布式锁不是银弹,最好的做法是业务本身设计为幂等(如使用唯一请求号、数据库唯一约束),再配合分布式锁作为最后防线。
-
测试环境:如果环境没有 Redis,Laravel 有一个测试用的
ArrayLock驱动(在config/cache.php中配置'default' => 'array'时支持),但不保证分布式,仅适合单进程测试。
- 是的,Laravel 防重入推荐使用分布式锁。
- 首选方案:
Cache::lock()(基于 Redis)。 - 适用场景:接口防重复提交、队列任务防重消费、库存扣减等并发写操作。
- 避免踩坑:合理设置锁超时时间、确保所有服务器使用同一 Redis、结合幂等设计。
如果你的项目正在使用 Laravel Octane(高性能常驻内存),还可以使用 Laravel\Octane\Contracts\DispatchesCoroutines 配合锁,但核心思路不变。