Laravel防重入用分布式锁吗

wen PHP项目 18

本文目录导读:

Laravel防重入用分布式锁吗

  1. 为什么需要分布式锁?
  2. Laravel 原生支持:Cache::lock()
  3. 队列场景:防止任务重入
  4. 更高级的用法(避免阻塞)
  5. 实现分布式锁的底层原理
  6. 注意事项

是的,Laravel 在处理防重入(即防止重复提交或重复执行)时,强烈推荐使用分布式锁,特别是当你的应用运行在多个服务器(负载均衡)或使用队列多进程消费时。

单纯的数据库唯一索引或Cache::add()(原子检查)只能解决部分场景,而基于 Redis 或数据库的分布式锁才是真正应对并发、跨进程、跨机器的重入问题的标准方案。

下面详细说明为什么需要以及如何在 Laravel 中实现。

为什么需要分布式锁?

  1. 跨进程/跨服务器:单机锁(如 synchronizedMutex)只能在同一个 PHP 进程内生效,如果启用了多个 php-fpm 进程或有多台服务器,无法阻止并发请求。
  2. 队列消费:Laravel 队列的 worker 通常启动多个进程,如果队列任务未正确处理幂等性,同一任务可能被多个 worker 同时消费导致数据重复。
  3. 接口防重:用户快速点击“提交订单”,两个请求到达不同服务器,数据库事务中检查库存时都可能通过,导致超卖。

Laravel 原生支持:Cache::lock()

从 Laravel 5.6 开始,框架内置了 原子锁(Atomic Locks)功能,只要配置了 redismemcached 驱动(推荐 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 原子命令(或类似机制),确保:

  1. 原子性:只有一个进程能成功设置 key。
  2. 超时自动释放:通过 EX(过期时间)防止进程崩溃导致死锁。
  3. 所有者识别:Laravel 会生成一个随机值(token)存储在锁中,释放时只有拥有该 token 的进程才能释放,避免误删其他进程的锁。

注意事项

  1. 锁超时时间:必须大于业务的预期执行时间,如果业务执行时间超过锁超时时间,锁会自动释放,其他请求可能进入,建议:

    • 预估业务执行时间,设置超时时间为其 2-3 倍。
    • 如果业务不可预估(如调用外部慢 API),考虑使用 续期机制(Laravel 不内置,需自己实现或使用 Laravel Octane / 其他库)。
  2. 使用同一缓存驱动:所有服务器必须配置同一个 Redis 实例(或集群),才能共享锁状态。

  3. 幂等性设计 + 分布式锁:分布式锁不是银弹,最好的做法是业务本身设计为幂等(如使用唯一请求号、数据库唯一约束),再配合分布式锁作为最后防线。

  4. 测试环境:如果环境没有 Redis,Laravel 有一个测试用的 ArrayLock 驱动(在 config/cache.php 中配置 'default' => 'array' 时支持),但不保证分布式,仅适合单进程测试。

  • 是的,Laravel 防重入推荐使用分布式锁
  • 首选方案Cache::lock()(基于 Redis)。
  • 适用场景:接口防重复提交、队列任务防重消费、库存扣减等并发写操作。
  • 避免踩坑:合理设置锁超时时间、确保所有服务器使用同一 Redis、结合幂等设计。

如果你的项目正在使用 Laravel Octane(高性能常驻内存),还可以使用 Laravel\Octane\Contracts\DispatchesCoroutines 配合锁,但核心思路不变。

抱歉,评论功能暂时关闭!