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

Laravel中如何解决保存预订API并发请求导致预订号重复的问题

问题根源

你遇到的重复预订号问题本质是读-改-写的非原子操作:多个并发请求同时执行generate_booking_id()时,会同时读取到相同的预订表总记录数,计算得到相同的后缀序号,最终生成重复的预订号。

Laravel框架下的可选解决方案

方案1:数据库唯一约束+失败重试(最简单可靠)

这是成本最低、容错性最高的方案,步骤如下:

  • 第一步:给bookings表的booking_number字段添加唯一索引,避免重复数据入库
    迁移代码示例:
    Schema::table('bookings', function (Blueprint $table) {
        $table->string('booking_number')->unique()->change();
    });
    
  • 第二步:修改保存逻辑,捕获唯一索引冲突异常,自动重试生成预订号
    代码示例:
    public function save_booking(Request $request){
        // 最多重试3次,避免无限循环
        return retry(3, function () use ($request) {
            $booking = new Booking;
            $booking->booking_number            = generate_booking_id();
            $booking->booking_type              = $request->input('bookingType');
            $booking->booking_date              = $request->input('bookingDate');
            $booking->booking_time              = $request->input('bookingTime');
            $booking->booking_status            = $request->input('bookingStatus');
            $booking->save();
            return $booking;
        }, 100, function ($exception) {
            // 只有唯一索引冲突异常才重试
            return $exception instanceof \Illuminate\Database\UniqueConstraintViolationException;
        });
    }
    
    这里用到Laravel自带的retry辅助函数,每次重试间隔100毫秒,只有唯一冲突时才触发重试。

方案2:单独维护序列号表+原子更新(适合对序号连续性要求高的场景)

如果要求预订号后缀必须连续无跳号,可以单独创建一张booking_sequences表,存储每个年月对应的当前最大序号,使用数据库原子更新保证计数唯一:

  • 表结构核心字段:
    • year_month:字符串类型,存储年月,如202105,添加唯一索引
    • current_seq:整型,存储当前年月对应的最大已用序号
  • 改造generate_booking_id逻辑:
    function generate_booking_id(){
        $yearMonth = now()->format('Ym');
        // 事务+行锁保证同一时间只有一个请求修改对应年月的序号
        $sequence = \DB::transaction(function () use ($yearMonth) {
            $seq = \DB::table('booking_sequences')
                ->where('year_month', $yearMonth)
                ->lockForUpdate()
                ->first();
            
            if ($seq) {
                \DB::table('booking_sequences')
                    ->where('year_month', $yearMonth)
                    ->increment('current_seq');
                return $seq->current_seq + 1;
            } else {
                \DB::table('booking_sequences')->insert([
                    'year_month' => $yearMonth,
                    'current_seq' => 1
                ]);
                return 1;
            }
        });
        return 'BN-' . $yearMonth . '-' . str_pad($sequence, 4, '0', STR_PAD_LEFT);
    }
    

方案3:使用Laravel原子锁(适合分布式部署场景)

如果是多节点分布式部署,可以用Laravel支持的Redis原子锁,保证同一时间只有一个请求能执行预订号生成逻辑:

public function save_booking(Request $request){
    // 获取锁,锁有效期5秒足够完成保存逻辑
    $lock = \Cache::lock('booking_generate_lock', 5);
    if (!$lock->get()) {
        abort(503, '服务器繁忙,请稍后再试');
    }
    try {
        $booking = new Booking;
        $booking->booking_number            = generate_booking_id();
        $booking->booking_type              = $request->input('bookingType');
        $booking->booking_date              = $request->input('bookingDate');
        $booking->booking_time              = $request->input('bookingTime');
        $booking->booking_status            = $request->input('bookingStatus');
        $booking->save();
        return $booking;
    } finally {
        // 强制释放锁避免死锁
        $lock->release();
    }
}
选型建议
  • 普通业务场景直接选方案1,代码改动最小,可靠性足够
  • 要求序号严格连续无跳号选方案2
  • 分布式多节点部署可选方案3,或方案1+方案2结合使用

内容的提问来源于stack exchange,提问作者Dhairyashil Nalawade

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.19 10:27:06