Laravel中如何解决保存预订API并发请求导致预订号重复的问题
问题根源
你遇到的重复预订号问题本质是读-改-写的非原子操作:多个并发请求同时执行generate_booking_id()时,会同时读取到相同的预订表总记录数,计算得到相同的后缀序号,最终生成重复的预订号。
Laravel框架下的可选解决方案
方案1:数据库唯一约束+失败重试(最简单可靠)
这是成本最低、容错性最高的方案,步骤如下:
- 第一步:给
bookings表的booking_number字段添加唯一索引,避免重复数据入库
迁移代码示例:Schema::table('bookings', function (Blueprint $table) { $table->string('booking_number')->unique()->change(); }); - 第二步:修改保存逻辑,捕获唯一索引冲突异常,自动重试生成预订号
代码示例:
这里用到Laravel自带的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; }); }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
相关产品推荐
相关产品推荐

