如何避免并行执行时生成重复发票编号?(附代码片段)
解决并发下重复发票编号的问题
你的代码在并发场景下会出现竞态条件——两个请求同时读取到相同的最大发票号,各自加1后插入,最终生成重复的编号。以下是几种可靠的解决方法:
方案一:利用数据库原子性操作(推荐)
创建一个专门的供应商发票计数器表(比如vendor_invoice_counters),让数据库通过原子操作处理编号递增,从根源避免应用层的读取-修改-写入循环:
// 开启事务 DB::beginTransaction(); try { // 对目标供应商的计数器行加排他锁,确保同一时间仅一个请求可修改 $counter = VendorInvoiceCounter::where('vendor_id', $request->user()->vendorId) ->lockForUpdate() ->first(); if ($counter) { $newInvoiceNumber = $counter->current_number + 1; $counter->update(['current_number' => $newInvoiceNumber]); } else { $newInvoiceNumber = 1; VendorInvoiceCounter::create([ 'vendor_id' => $request->user()->vendorId, 'current_number' => $newInvoiceNumber ]); } // 创建发票编号记录 $invoiceNumber = InvoiceNumbers::create([ 'invoice_number' => $newInvoiceNumber, 'vendor' => $request->user()->vendorId ]); DB::commit(); } catch (\Exception $e) { DB::rollBack(); throw $e; }
lockForUpdate()会锁定选中的行,阻止其他请求同时修改,彻底避免并发冲突。
方案二:联合唯一约束+重试机制
给InvoiceNumbers表的vendor和invoice_number字段添加联合唯一索引,让数据库直接拒绝重复插入,再通过捕获异常重试解决冲突:
- 先添加联合唯一索引(迁移文件):
Schema::table('invoice_numbers', function (Blueprint $table) { $table->unique(['vendor', 'invoice_number']); });
- 修改代码加入重试逻辑:
$maxAttempts = 3; $attempt = 0; do { $attempt++; try { $invoiceNumber = InvoiceNumbers::where('vendor', $request->user()->vendorId) ->orderByDesc('invoice_number') ->limit(1) ->first(); $newInvoiceNumber = $invoiceNumber ? $invoiceNumber->invoice_number + 1 : 1; $invoiceNumber = InvoiceNumbers::create([ 'invoice_number' => $newInvoiceNumber, 'vendor' => $request->user()->vendorId ]); break; } catch (\Illuminate\Database\QueryException $e) { // 仅捕获MySQL唯一约束冲突错误(错误码1062),超出重试次数则抛出异常 if ($e->getCode() != 1062 || $attempt >= $maxAttempts) { throw $e; } usleep(100000); // 短暂休眠后重试 } } while ($attempt < $maxAttempts);
这种方式无需锁表,适合并发量不高的场景。
方案三:悲观锁锁定查询
在读取最大发票号时加共享锁,确保读取到的是最新数据,同时阻止其他请求的修改操作:
DB::beginTransaction(); // 加共享锁,确保当前读取的数据不会被其他事务修改 $invoiceNumber = InvoiceNumbers::where('vendor', $request->user()->vendorId) ->orderByDesc('invoice_number') ->limit(1) ->shareLock() ->first(); $newInvoiceNumber = $invoiceNumber ? $invoiceNumber->invoice_number + 1 : 1; $invoiceNumber = InvoiceNumbers::create([ 'invoice_number' => $newInvoiceNumber, 'vendor' => $request->user()->vendorId ]); DB::commit();
该方案并发高时会导致锁等待,性能不如方案一,仅适合低并发场景。
内容的提问来源于stack exchange,提问作者d3v1anX
相关产品推荐
相关产品推荐

