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

如何避免并行执行时生成重复发票编号?(附代码片段)

解决并发下重复发票编号的问题

你的代码在并发场景下会出现竞态条件——两个请求同时读取到相同的最大发票号,各自加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字段添加联合唯一索引,让数据库直接拒绝重复插入,再通过捕获异常重试解决冲突:

  1. 先添加联合唯一索引(迁移文件):
Schema::table('invoice_numbers', function (Blueprint $table) {
    $table->unique(['vendor', 'invoice_number']);
});
  1. 修改代码加入重试逻辑:
$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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 22:55:24