You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel高并发下事务无法防止兑换码重复分配的解决方案

问题根因

单纯用事务包裹逻辑无法防重复的核心原因是普通事务没有解决并发查询的竞态问题:高并发场景下,多个请求会同时执行getNextAvailableCode()查询,读到同一条未分配的兑换码记录,后续每个请求都会独立执行标记已分配、写入关联表的操作,自然会出现同个码分给多个用户的问题。

待排除的无效/低性价比方案
  • 直接锁全表:会阻塞codes表的所有读写请求,高并发下数据库吞吐量会断崖式下跌,完全不可取
  • 写入UserCode前先查是否存在同code_id的绑定记录:属于典型的「检查时-使用时」竞态漏洞,两个请求同时查询时都会判定记录不存在,后续依然会同时写入重复数据。就算给code_id加唯一索引,后写入的请求会直接触发数据库唯一键冲突报错,会导致大量用户领码失败,不符合平滑交互的要求。
高并发下的平滑实现方案

核心思路是用InnoDB行级排他锁+跳过已锁行机制,只锁定当前要分配的单条兑换码记录,不锁全表,同时从机制上避免多个请求争抢同一个码,用户无感知不会频繁收到报错。

第一步:加数据库兜底约束

先给users_codes表的code_id字段添加唯一索引,从数据库层面强制一个兑换码只能绑定一条用户记录,作为所有逻辑的最后防线,哪怕代码出现极端异常也不会出现重复分配问题。

第二步:改写领码逻辑

核心是把「查询可用码」和「加行锁」做成原子操作,配合事务重试、跳过已锁行的特性实现无冲突领码,代码如下:

// 事务第二个参数设置自动重试次数,遇到死锁等数据库异常自动重试,用户无感知
DB::transaction(function () use ($user) {
    // 原子查询+加排他锁,SKIP LOCKED表示直接跳过已经被其他事务锁住的记录,不等待锁
    // 按自己的发码规则调整排序逻辑,比如按id正序就是按入库顺序发码
    $code = Codes::where('allocated', 0)
        ->orderBy('id', 'asc')
        ->lock('FOR UPDATE SKIP LOCKED')
        ->first();

    // 无可用码的业务处理
    if (!$code) {
        throw new \Exception('兑换码已发放完毕');
    }

    // 标记兑换码为已分配
    $code->allocated = 1;
    $code->save();

    // 写入用户-兑换码关联记录
    $uc = new UserCode();
    $uc->user_id = $user->id;
    $uc->code_id = $code->id;
    $uc->save();
}, 3);

方案关键说明

  • 如果你使用的是MySQL 5.x版本不支持SKIP LOCKED语法,把lock('FOR UPDATE SKIP LOCKED')替换成Laravel自带的lockForUpdate()即可,同样可以解决重复分配问题,只是高并发下会有极短的行锁等待,因为只锁单条记录,性能影响可以忽略。
  • SKIP LOCKED机制下,每个请求进来都会直接拿到第一个未被其他事务锁定的可用兑换码,从根本上避免了多个请求争抢同一个码的情况,正常流程下不会触发唯一键冲突,用户不会领到报错。
  • 事务自动重试机制会处理极端场景下的死锁问题,整个重试过程对用户透明,不需要用户重复点击操作。

超大规模并发可选优化

如果你的领码接口QPS超过5000+,可以提前把可用兑换码按发放顺序推入Redis List队列,领码时直接从队列左侧POP元素做分配,性能比数据库行锁方案高一个量级,但需要额外做队列和数据库的数据一致性校验,中低并发场景下用上面的数据库方案足够,维护成本更低。

内容的提问来源于stack exchange,提问作者Giles Bennett

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 17:09:14