Laravel 用 Cache::lock 实现分布式锁:拦截并发重复提交与超卖

test12026-10-075 次阅读

单机锁在多实例下会失效

PHP 进程内用变量做标记来防并发,在 FPM 单进程模型下勉强够用,但一旦上了多台应用服务器或者 Swoole/Octane 常驻内存,进程间的标记互不感知,竞态立刻出现。下单、扣库存、发券这类"同一动作不能被并发执行两次"的场景,需要一个所有实例都能看到的锁。Laravel 的 Cache::lock 底层走 Redis 或 memcached,天然是分布式的,正好补上这个缺口。

防并发重复提交

用户手快点了两次提交,或者前端重试没拦住,后端就必须自己兜底。用 Cache::lock 给"用户 ID + 动作"加一把带超时的锁,拿到锁才继续,拿不到直接拒绝。

use Illuminate\Support\Facades\Cache;

public function checkout(Request $request)
{
    $userId = $request->user()->id;
    $lock = Cache::lock("checkout:{$userId}", 10);

    if (!$lock->get()) {
        return response()->json(['message' => '操作进行中,请稍候'], 429);
    }

    try {
        $order = $this->createOrder($userId);
        return response()->json(['order_id' => $order->id]);
    } finally {
        $lock->release();
    }
}

锁的第二个参数是生存时间,单位秒。即使代码抛异常没走到 release,10 秒后锁也会自动过期,不会把用户永久卡死。finally 里释放是为了正常路径也尽快让出锁。

block:拿不到锁就等一会儿

有些场景不适合直接拒绝,而是希望"稍等锁释放再执行"。block 方法第一个参数是等待秒数,第二个是拿到锁后执行的闭包。

public function deduct(int $skuId, int $qty): bool
{
    return Cache::lock("sku:{$skuId}", 5)->block(3, function () use ($skuId, $qty) {
        $stock = DB::table('stocks')
            ->where('sku_id', $skuId)
            ->lockForUpdate()
            ->value('qty');

        if ($stock < $qty) {
            return false;
        }

        DB::table('stocks')
            ->where('sku_id', $skuId)
            ->decrement('qty', $qty);
        return true;
    });
}

为什么扣库存要锁加事务再加行锁

只靠 Cache::lock 已经能拦住并发进入扣减逻辑,但数据库层面仍建议配合 lockForUpdate 的行锁:它保证读库存和扣减之间不会被别的连接插队改掉库存值,避免"超卖"。Cache 锁负责跨实例串行化,数据库行锁负责单点一致性,两层叠起来才稳。

使用边界与坑

  • 锁的键要有业务唯一性,比如"用户 ID + 订单类型",粒度太粗会不必要地串行化,太细又拦不住真正的竞态。
  • 缓存驱动必须支持原子加锁,Redis 和 memcached 都支持;用文件或数组驱动时锁只在当前进程有效,多实例下失效。
  • 代码异常时务必释放锁,最好用 try/finally 或 block 闭包,别让锁靠过期兜底成为常态。
  • 锁过期时间要大于业务最坏执行时长,否则会出现"锁已过期但旧任务还在跑、新任务又进来"的二次竞态。

小结

Cache::lock 把分布式环境下的互斥变得一行搞定:get 拿不到就拒绝,block 拿不到就等。配合数据库行锁,能稳妥解决重复提交和库存超卖这类高频并发问题,是 Laravel 工程里值得固定的标准写法。

T

test1

文章作者

单机锁在多实例下会失效 PHP 进程内用变量做标记来防并发,在 FPM 单进程模型下勉强够用,但一旦上了多台应用服务器...

分类
技术
发布时间
2026-10-07
字数
约 1901 字
阅读
5 次