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

并发生成唯一发票编号时如何避免冲突?

解决Stripe Webhook并发下发票编号重复的最优方案

这确实是分布式系统中生成全局唯一连续编号的典型痛点,靠Stripe重试虽然能解决问题,但体验和效率都不够好。结合你用PostgreSQL的场景,给你几个更靠谱的解决方案,按推荐优先级排序:

1. 用PostgreSQL年份序列(Sequence)实现原子编号生成

PostgreSQL的序列本身就是为原子性生成连续数字设计的,完全能解决并发冲突问题。你可以按年份创建独立序列,比如seq_invoice_2024、seq_invoice_2025,每年年初自动创建(或首次使用时动态创建)。

具体操作:

  • 创建序列(以2024年为例):
    CREATE SEQUENCE seq_invoice_2024 START WITH 1 INCREMENT BY 1;
    
  • 生成发票编号时,通过SQL原子性获取下一个序列值并格式化:
    SELECT CONCAT(EXTRACT(YEAR FROM CURRENT_DATE), '-', LPAD(NEXTVAL('seq_invoice_' || EXTRACT(YEAR FROM CURRENT_DATE))::TEXT, 5, '0')) AS invoice_number;
    
    (可以把这段逻辑封装成PostgreSQL函数,调用更方便)

优点:

  • 数据库原生支持,性能极高,完全避免并发冲突
  • 实现简单,不需要复杂的事务或锁逻辑
  • 自动维护连续编号,不用担心手动计算出错

2. 用计数器表+行锁实现灵活的编号控制

如果需要更灵活的控制(比如手动调整编号、处理特殊场景),可以建一个专门的发票计数器表,通过SELECT FOR UPDATE实现行级锁,保证并发下的原子性。

具体步骤:

  1. 创建计数器表:
    CREATE TABLE invoice_counter (
        year INT PRIMARY KEY,
        last_number INT NOT NULL DEFAULT 0
    );
    
  2. 生成编号的事务逻辑:
    BEGIN;
    -- 锁定当前年份的计数器行,其他请求需等待当前事务完成
    SELECT last_number FROM invoice_counter WHERE year = EXTRACT(YEAR FROM CURRENT_DATE) FOR UPDATE;
    -- 计算新编号(如果是当年第一条,初始化last_number为1)
    INSERT INTO invoice_counter (year, last_number)
    VALUES (EXTRACT(YEAR FROM CURRENT_DATE), 1)
    ON CONFLICT (year) DO UPDATE SET last_number = invoice_counter.last_number + 1
    RETURNING CONCAT(year, '-', LPAD(last_number::TEXT, 5, '0')) AS invoice_number;
    COMMIT;
    

优点:

  • 可以随时查看和调整当年的编号进度
  • 跨年时自动初始化新的计数器行
  • 行锁粒度小,对性能影响极低

3. 必须加上Webhook事件的幂等校验

不管用哪种编号生成方案,都要加上Stripe事件的幂等处理——因为Stripe会在Webhook响应失败时自动重试,即使你解决了编号冲突,重复事件也可能导致重复创建发票。

实现方式:

  • 在数据库中创建一个stripe_events表,存储已处理的事件ID:
    CREATE TABLE stripe_events (
        event_id TEXT PRIMARY KEY,
        processed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
    );
    
  • 处理Webhook时,先检查事件ID是否已存在:
    # 伪代码示例
    def handle_stripe_event(event):
        # 先校验幂等
        if db.query("SELECT 1 FROM stripe_events WHERE event_id = %s", (event.id,)):
            return 200  # 已处理过,直接返回成功
        
        # 生成发票编号、创建发票逻辑...
        
        # 最后记录事件ID
        db.execute("INSERT INTO stripe_events (event_id) VALUES (%s)", (event.id,))
        return 200
    

这样即使Stripe重试,也不会重复处理同一事件,从根源上避免重复发票的问题。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.06 12:22:27