并发生成唯一发票编号时如何避免冲突?
解决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原子性获取下一个序列值并格式化:
(可以把这段逻辑封装成PostgreSQL函数,调用更方便)SELECT CONCAT(EXTRACT(YEAR FROM CURRENT_DATE), '-', LPAD(NEXTVAL('seq_invoice_' || EXTRACT(YEAR FROM CURRENT_DATE))::TEXT, 5, '0')) AS invoice_number;
优点:
- 数据库原生支持,性能极高,完全避免并发冲突
- 实现简单,不需要复杂的事务或锁逻辑
- 自动维护连续编号,不用担心手动计算出错
2. 用计数器表+行锁实现灵活的编号控制
如果需要更灵活的控制(比如手动调整编号、处理特殊场景),可以建一个专门的发票计数器表,通过SELECT FOR UPDATE实现行级锁,保证并发下的原子性。
具体步骤:
- 创建计数器表:
CREATE TABLE invoice_counter ( year INT PRIMARY KEY, last_number INT NOT NULL DEFAULT 0 ); - 生成编号的事务逻辑:
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
相关产品推荐
相关产品推荐

